long long nth_perfect_power(int n){
if(n == 1) return 1;
auto limit = 1e10;
set<long long> powers;
for(long long m = 2; m * m < limit; m++){
long long power = m * m;
while(power < limit){
powers.insert(power);
power *= m;
}
}
auto it = powers.begin();
std::advance(it, n-2);
return *it;
}
이 소스는 퍼포먼스에 문제가 있습니다.
set은 고성능으로 sorting된 형태로 저장하지만 위와 같은 소스에서는 set으로 인하여 탐색과 삽입을 수십만번 반복함으로써 성능상의 문제가 발생합니다.하여 저장시에는 저장만하고 마지막에 한번 sorting하는 방식으로 바꿔봅니다.
long long nth_perfect_power2(int n){
if(n == 1) return 1;
auto limit = 1e10;
vector<long long> powers;
for(long long m = 2; m * m < limit; m++){
long long power = m * m;
while(power < limit){
powers.push_back(power);
power *= m;
}
}
std::sort(powers.begin(), powers.end());
return powers[n-2];
}
n이 늘어 나면 늘어 날수록 memory와 cpu비용이 함께 증가하는 아주 안좋은 알고리즘 되겠다.
int to string역시 O(n)이므로 사실상의 시간 복잡도는 O(n^n)이다.
그러나
이걸 아키텍처 적으로 플어본다면 문자열 str은 상수와가 가능하고 상수화하고 n번째 문자를 찾는다면 결국은 O(1)의 완벽한 알고리즘이된다. 상황과 시스템에 따라서 이런 알고리즘도 상당한 퍼포먼스를 발휘할수도 있다. 만약 str을 생성하는 문제가 복잡하고 수식으로 계산하기 힘든경우에는 웜업시간에 str을 생성하고 매 요청때마다 결과를 도출하는것도 사실상 더 많이 쓰이는 방법되겠다.
그래도 알고리즘만 연구하시는 분들은 최적의 공간/시간 복잡도를 추구하므로 최적의 공간/시간 복잡도를 가지는 알고리즘을 연구해보자!!
2. 수치해석적 알고리즘
- 일단 문제를 해결하기 위해서 규칙을 찾아내야한다.
- 문자열을 만드는 숫자를 "k"라고 하고 요구하는 위치값을 "n"이라하자.
- k의 자릿수를 digit이라하자
- 각 digit별 자리수 count(digit) = 9 * 10 ^ (digit-1) 이다.
세계인의 타임킬링게임 "지뢰찾기"게임에서 특정 블럭을 눌렀을때 막혀있지 않은 최대 면접을 구하여 OPEN해줄때 사용할 수 있는 면적구하기 알고리즘 입니다. Graph, DFS에 관련해서는 미로찾기(graph traversal)을 먼저 읽고 오면 도움이 됩니다.
이전 포스트에서 언급한바와 같이 Graph - DFS ( Depth First Search ) 알고리즘은 완전 탐색 즉 모든 노드를 방문할 수 있는 알고리즘입니다. 이 알고리즘은 조금 응용해서 방문할 수 있는 모든 노드를 방문하면 결국은 방문수가 해당 node group의 넓이가 됩니다.
문제
아래와 같은 지도가 존재할때 빈칸으로 이루어진 영역중 최대 크기 영역의 크기를 구해봅시다.
FOUND (banana) : b - a - n - a - n - a FOUND (banana) : b - a - n - a - na FOUND (banana) : b - a - na - n - a FOUND (banana) : b - a - na - na FOUND (banana) : ba - n - a - n - a FOUND (banana) : ba - n - a - na FOUND (banana) : ba - na - n - a FOUND (banana) : ba - na - na
Recursive는 상당히 많은 알고리즘 문제나 면접문제에 나오는 알고리즘 이지만 사실 거시적 소프트웨어 구현측면에서는 잘 사용하지 않는 기법이다. 이유는
사용되는 메모리 스택의 크기를 가늠하기 어렵고
스택 오버플로을 컨트롤하기 어렵다.
모니터링 또한 어렵다.
위의 내용을 이해하기 위해 recursive가 무언가에 대해 얘기하자면... 유명한 N!을 구하는 프로그램을 짜보자
N! = N * (N-1) * (N-2) * ... * 2 * 1
이걸 조금 바꿔보면
N! = N * (N-1)!
이지용?
이를 코드로 구현하면 ...
int factorial(int n){
if(n == 1) return 1;
return n * factorial(n-1)
}
N!를 구하는 문제가 간단한 4줄로 구현되었다.
이코드에 stack의 S도 나오지 않았지만 실제는 stack이 사용되었다.
compiler는 function call시 현재의 argument를 stack에 저장하고 새로운 function으로 진입한다.
함수가 리턴되면 저장된 argument를 pop하여 현재의 local 변수들도 대체하고 연산을 수행한다.
결국은 n==1이 될때까지 stack에 저장하고 순차적으로 n!를 계산한다.
여기서 문제는 stack은 compiler에 의해 관리된다는 점이고 heap영역과 공유되기 때문에 깊이가 깊어지면 stack overflow과 발생하고 panic에 빠지는데 control할수있는 방법이 없다. 실행되는 시점에 남은 stack영역을 check하기도 쉽지 않으며 ... abort또한 어렵다.
배보다 배꼽이 더 큰 상황???
사실 N!의 경우 for문으로 간단히 계산할수있지만 recursive와 stack에 대한 내용이므로 어렵게 stack을 이용한 방법으로 바꿔 보겠다.
void push(int n);
int pop(); // returns -1 if stack is empty
int factorial(int n){
while(n > 1){
push(n--);
}
int n;
int result = 1;
while((n=pop()) > 0){
result *= n;
}
return result;
}