빅 오 분석법 O(n)

자신이 푼 코드의 시간효율과 공간복잡도에 대해서도 명확히 말할 수 있는 개발자가 진정한 개발자라고 생각한다. 

그런 의미에서는 시간 효율과 같은 경우에 대충 for문으로 때려 잡고 있던(사실 대개 반복문이 시간분석의 핵심이긴하다) 

나에게는 간단하지만, 정확하게 분석 할 수 있는 능력이 필요하다고 생각한다.


빅 오 분석법

"입력 값에 따라 무엇을 n으로 놓을지 결정하고 나면 n개의 입력된 값을 몇 번이나 확인해봐야 하는지를 n의 식으로 표현" 

여기서 확인하다??라는 것이 무엇인지 애매하다. 

일반적으로 입력된 값에 상수를 더한다거나 새로운 입력 아이템을 만든다거나 입력 값을 삭제한다거나 비교한다던가 하는 것을 "확인" 이라고 표현 할 수 있겠다. 

하나의 간단한 코드를 보면서 빅 오 분석법을 이해해보고자 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int findMax(int arr[], int n){
    /* 예외 처리 */
    if (n < 1)
        return -1;
    /* 비교 할 수 */
    int maxvalue = arr[0];
 
    /* 2번째 수 부터 끝까지 최대값과 비교해서 계속 갱신해준다.*/
    for (int i = 1; i < n; i++){
        if (maxvalue < arr[i])
            maxvalue = arr[i];
    }
    return maxvalue;
}
cs


위의 코드는 주어진 배열에서 최대값을 찾는 코드이다. 여러 방법이 있겠지만 그 중에 위의 코드는

최대값을 찾는 변수를 할당해 놓고 모든 수와 한 번씩만 비교해서 가장 큰 값을 반환하는 코드이다.

자, 입력된 항목의 자료들이 한 번 씩만 비교하면 되므로 O(n)의 시간복잡도가 계산된다. 

명확히 말하면 맨 처음에 자료가 0개인지 아닌지를 판별하는데 1이 소모 되었고,

maxvalue를 첫번째 원소로 초기화하는데 1이 소모 되었으므로 

O(n + 2)라고 할 수 있다. 하지만 2라는 상수는 충분히 커진 수에서는 별로 의미가 없다.

우리는 위의 코드처럼 고작 6개의 수가 얼마나 시간이 걸리는지를 계산할려고 시간복잡도를 구하는 것이 아니기

때문이다. 우리는 수백, 수억개의 숫자들 중에서 최대값을 찾는데 걸리는 시간에 대한 복잡도를 구하는 것이다. 

예를 들어 O(10000000000000 + 2)에서 2는 의미가 있나? 없다고 판단해도 된다.

그래서 시간 복잡도는 O(n)으로 계산되는 것이다. 


이중 for문에서도 비슷하게 계산 가능하다. 

상수 시간이 걸리는 부분은 과감히 생략되고 실질적으로 n개의 자료들이 이중 for문의 연산이 수행되면

O(n^2)이 된다. (블로그에 제곱 표기가 좋지 않아 이렇게 표현한다)



* 시간 복잡도를 구하는 방법 정리

1. 빅-오(Big-Oh)표기법

최악의 경우를 복잡도를 구하는 경우를 말한다. "최악의 경우 시간 복잡도는 O(n)이다" 이런식

2. 빅-오메가(Big-Omega) 표기법

빅 오와 반대되는 개념. 최선의 경우의 복잡도를 구하는 것을 말한다. "최선의 경우 Ω(n)이다."이런식

3. 세타(Theta) 표기법

빅오와 빅오메가의 중간인 평균적인 복잡도를 구하는 경우를 말한다.







'알고리즘' 카테고리의 다른 글

버블정렬  (0) 2015.09.26
재귀호출 - Recursive Call  (0) 2015.09.24
조합알고리즘의 최적화  (0) 2015.04.30
2차원 배열의 경우의 수 구하기  (0) 2015.04.28
증가하는부분수열(Longest Increase Subsequence)  (0) 2015.04.26
Posted by slender ankles
,

조합알고리즘

nCr 을 구현해야 됐던 적이 있었나?

경험적으로 재귀적으로 수행했을 때도 충분히 답이 나오는 문제만 풀어서 그런지 없었다. 


1010 다리 놓기 라는 문제를 통해 

그냥 재귀탐색과 DP를 통한 메모이제이션 탐색의 엄청난 시간 차이를 경험했다. 


조합알고리즘에 대해서 간단히 설명하자면

n개 중에서 r개를 뽑는 경우의 수를 nCr 이라고 표현하고 

수학에서는 (n - r)! / r!     또는      n! / r! * (n-r)!으로 계산 해주지 않았나?

=> 6C3 => 6 * 5 * 4 / 3 * 2 * 1 이었다.

하지만 수학이 아닌 컴퓨터에서는 

이렇게 팩토리얼 개념을 적용시켜주면 자칫 메모리를 엄청나게 

쓰다가 스택 오버플로우가 날 수 있다.

n이 충분히 커지게 되면 n!은 기하급수적으로 증가하게 된다. 


그래서 식을 다음과 같이 바꾸어주게 된다. 

nCr = (n - r + 1) / r * nCr-1

nC0 = 1(r ==0일때, basecase)


그렇다면 이 것을 cache를 써서 다음과 같이 표현해주면 완벽하다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string.h>
using namespace std;
 
int testcasenum;
int n, r;
int cache[31][31];
 
int bino(int n, int r){
    if (r == || r == n){
        return 1;
    }
    if (cache[n][r] != -1
        return cache[n][r];
    
    return cache[n][r] = bino(n - 1, r - 1+ bino(n - 1, r);
}
 
int main(){
    cin >> testcasenum;
    for (int t = 1; t <= testcasenum; t++){
        memset(cache, -1sizeof(cache));
        cin >> r >> n;
        cout << bino(n, r) << endl;        // nCr을 구한다.
    }
    return 0;
}
 
cs




'알고리즘' 카테고리의 다른 글

재귀호출 - Recursive Call  (0) 2015.09.24
빅 오 분석법 O(n)  (0) 2015.07.23
2차원 배열의 경우의 수 구하기  (0) 2015.04.28
증가하는부분수열(Longest Increase Subsequence)  (0) 2015.04.26
퇴각 검색(BackTracking)  (0) 2015.04.22
Posted by slender ankles
,

2 * 2 배열에 1,2를 채우는 경우의 수하기.

2 * 2 * 2 * 2 => 16가지의 경우의 수가 나와야 한다.

그런데 내가 코딩한 거에는 96가지가 나왔다. 무엇인가 잘 못 코딩해준것이었다. 


항상 2차원 배열에 대한 경우의 수 구하는 문제를 만나면 어디선가 루프를 잘 못 돌고 

그랬었는데, 확실히 내가 잘 못된 코드를 통해 풀고 있었다는 것을 깨달았다. 


2차원 루프에서 만약에 조건을 만나면 재귀 탐색 시작하고 무조건 빠져나와야 한다는 것이 중요했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;
 
int arr[2][2];
int cnt = 0;
 
bool check(){
    for (int i = 0; i < 2; i++){
        for (int j = 0; j < 2; j++){
            if (arr[i][j] == 0)
                return false;
        }
    }
    return true;
}
 
void makearr(int x_idx, int y_idx){
    for (int k = 1; k <= 2; k++){
        arr[x_idx][y_idx] = k;
 
        bool flag = false;        
        for (int i = 0; i < 2; i++){
            for (int j = 0; j < 2; j++){
                if (arr[i][j] == 0) { 
                    makearr(i, j);
                    flag = true;        // flag를 통해 조건 만나면 반드시 빠져나오게
                    break;            // flag를 통해 조건 만나면 반드시 빠져나오게
                }
            }
            if (flag) break;    // flag를 통해 조건 만나면 반드시 빠져나오게
        }
        if (check()){
            cnt++;
            for (int i = 0; i < 2; i++){
                for (int j = 0; j < 2; j++){
                    cout << arr[i][j] << " ";
                }
                cout << endl;
            }
            cout << "--------------------------" << endl;
        }
    }
    arr[x_idx][y_idx] = 0;
}
 
int main(){
    makearr(00);
    cout << cnt << endl;
    return 0;
}
cs


Posted by slender ankles
,
동적 계획법에 대한 예 중에 대표적인 것이 증가하는 부분 수열 문제이다. 
감소하는 부분 수열 문제 역시 둘 다 똑같은 개념이라고 할 수 있겠다. 
예를 들어 

수열

10, 20, 40, 30, 70, 50, 60이 있다고 한다면

이 중에서 증가하는 부분 수열 중에 가장 긴 것을 찾아라. 

라는 것이 증가하는 부분 수열이라고 하겠다. 

-----n^2 방법--------

간단히 n^2으로 풀 수 있는 소스를 설명하자면, 

(이 부분도 간단히 생각해내기 어려웠다. 동적계획법으로 푸는 n^2 이다)

정말 간단하고 쉬운 방법은 n! 완전탐색이겠다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
 
int arr[7= { 10204030705060 };
int dy[8];
int n = 7;
 
int main(){
    for (int i = 1; i <= n; i++){
        for (int j = 0; j < i; j++){
            if (arr[j] < arr[i] && dy[i] < dy[j] + 1){
                dy[i] = dy[j] + 1;
            }
        }
    }
        sort(dy, dy + n);  // 중요
    cout << dy[n - 1<< endl;
 
    return 0;
}
cs


10, 20, 40, 30, 70, 50, 60

기준이 되는 i값을 빨간색, 

그 앞의 값들 j값들을 주황색으로 표현해서 설명하자면, 

(1) i = 1 일 때

10, 20, 40, 30, 70, 50, 60

=> dy[7] = {0, 1, 0, 0, 0, 0, 0};

(2) i = 2일 때

1020, 40, 30, 70, 50, 60

=> dy[7] = {0, 1, 2, 0, 0, 0, 0};

(3) i = 3일 때

102040, 30, 70, 50, 60

=> dy[7] = {0, 1, 2, 2, 0, 0, 0};

(4) i = 4일 때

10204030, 70, 50, 60

=> dy[7] = {0, 1, 2, 2, 3, 0, 0};

(보충설명) 70이 가지는 증가하는 부분 수열 10, 20, 40, 70이다. 

.......


써보면서 알게 되었는데, 최대 증가하는 부분 수열은 연속된 것을 고려하지 않는 듯 하다. 

연속 된 것을 찾기 위한 알고리즘은 아닌 듯 하다. 


20보다 작은 인덱스들을 처음부터 돌아다니면서 

20보다 작고 (arr[i] > arr[j]) ,현재 자신이 가지고 있는 증가수열의 개수보다 크면(dy[i] < dy[j] + 1) 

갱신

.....

Sort(정렬)를 반드시 해야 한다. 

대충 감이 올지 모르겠다. 나도 글을 써보니까 이해가 될 듯 하다. 


-----n*logn 방법--------

1)Lower Bound 방법

Lower Bound란 허용되는 하한값을 가진다는 것을 말한다. 

결국 입력 되는 값들을 고대로 인덱스로 가지면서 연산을 해주면 된다는 것이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
 
int arr[5= {52413};
int dy[110000];
int n = 5;
 
int main(){
    for (int i = 0; i < n; i++){
        dy[arr[i]] = dy[arr[i] - 1+ 1;
    }
    for (int i = 0; i < 5; i++){
        cout << dy[arr[i]] << " ";
    }
    cout << endl;
    return 0;
}                                                            
cs



Posted by slender ankles
,

반복하는데 귀신인 컴퓨터에 적당한 방법이다. 가능한 경우를 모두 검사(exhaustive search)하는 무식한(?)방법이다.

답 내는 것이 문제가 아니라 얼마 만큼의 빠른 시간에 작업을 끝낼수 있는 가를 고민해야 되는 파트.

가능성이 없는 것은 미리 잘라버리는 것이 관건이다. 예를 들어, 3 번의 시합에서 2 번 우승하면 이기는 게임에서 미리 2 번 이기면 나머지 한 번은 할 필요가 없다. 이런 가능성을 생각해서 미리 많이 잘라버리면 그 만 큼의 실행시간이 빨라진다.

가능한 경우를 모두 검사하는 방법에는 보통 두 가지 유형의 문제로 나뉠수가 있다.

  • 부분집합형 백트래킹
  • 팩토리얼형 백트래킹

예를 들어 , 주머니 속에 7 개의 공이 있고 , 각 공에는 번호가 부여되어 있다.


부분집합형

주머니에서 몇 개의 공을 꺼내어 합이 13 이 공들을 찾는 경우에는 2^7 가지의 부분집합을 모두 검사해야 한다.

한 개 뽑는 경우

  • 6
  • 2
  • 9
  • ..
두 개 뽑는 경우
  • 6 , 2
  • 6 , 9
  • 6 , 8
  • ...
세 개 뽑는 경우
  • 6 , 2 , 9
  • 6 , 2 , 8
  • ...
...

7 개 모두 뽑는 경우

  • 6, 2, 9 , 8 , 3 , 4 , 7


팩토리얼 형

7 개의 공을 임의로 나열했을 때 인접한 수의 곱이 최대 인 경우는 어떤 경우인가를 알고자 할 때 7! 가지의 모든 경우를 생각해야 하므로 이는 팩토리얼 형 문제라고 생각할 수 있다.
  • 6 2 9 8 3 4 7
  • 6 2 9 8 3 7 4
  • ...


(문제) sum of subset 

6 , 1 , 9 , 8 , 3 , 4 , 7

총 합은 38 , 합은 반은 19 이다.

이 수들을 적절히 뽑아 19 를 만들 수 있는 방법의 수를 구하는 문제.

부분 집합형 문제의 기본 틀

첫째 원소, 둘째 원소 2 개 있을 때 부분집합의 수는 4 개이다.

  • 2 가지 모두 포함하는 경우 (o o)
  • 첫 원소 포함 , 둘째 원소 포함하지 않는 경우 (o x)
  • 첫 원소 포함하지 않고 , 둘째 원소 포함하는 경우(x o)
  • 2 가지 모두 포함하지 않는 경우 (x x)

o 를 1 로 x 를 0 으로 놓으면 ,

  • 1 , 1
  • 1 , 0
  • 0 , 1
  • 0 , 0

원소의 수가 2 개 일 때

이를 구현하기 위해 배열(배열명을 include[] 라 하자)을 사용하여

원소의 수가 3 개일 때



(샘플 문제)수의 부분합 문제로 조금씩 실행 속도를 높여 보기로 하자. 문제는 전제 합의 반 50 이되는 원소를 골라내는 문제이다. 
주어지는 데이터는 40 20 30 10 (수의 부분합 풀이)



Posted by slender ankles
,

그 동안 난해했던 이러한 류의 코딩하는 방법을 조금은 터득한 것 같아 정리해둘려고 한다. 

1) 항상 몇 가지 나열된 원소(숫자)를 중복을 허락하지 않고 모든 경우의 수를 구해야 할 때, 

=> n!

2) 몇 가지 나열된 원소(숫자)로 만들 수 있는 모든 경우의 수 구하기

=> n * n * n * n * ......

위와 같은 수행과정이 필요할 때 재귀적으로 코딩하는 것이 만만치 않았다. 

단지 for문을 돌면서 수행 할 수도 있다.(완전 탐색의 원소의 개수를 완벽히 알 수 있을 때 말이다.)

하지만 그렇지 않을 때에는 난 감했던 적이 많았는데 다음과 같이 코딩해주면 된다. 

이 것은 지속적으로 반복기억해주어야 겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
using namespace std;
 
int n;
int arr[4= {1234};
bool visited[4];
int result[4];
int combination_count = 0;
 
// 4가지 수를 중복을 허락하지 않고 모든 경우의 수 구하기 => 4!
// 순열 혹은 조합? 응용 가능?
void combination(int idx){
    if (idx >= 4){
        for (int i = 0; i < 4; i++){
            cout << result[i] << " ";
        }
        cout << endl;
        combination_count++;
        return;
    }
    for (int i = 0; i < 4; i++){
        if (!visited[i]){
            result[idx] = arr[i];
            visited[i] = true;
            combination(idx + 1);
            visited[i] = false;
        }
    }
}
 
// 4가지 수로 만들 수 있는 모든 경우의 수 구하기 => 4 * 4 * 4 * 4
void recursive(int idx){
    if (idx >= 4){
        for (int i = 0; i < 4; i++){
            cout << result[i] << " ";
        }
        cout << endl;
        return;
    }
    for (int i = 0; i < 4; i++){
        result[idx] = arr[i];
        recursive(idx + 1);
    }
}
 
int main(){
    // recursive(0);
    combination(0);
    cout << combination_count << endl;
    return 0;
}
cs


3) m가지의 수들로 n자리 수를 중복을 허락하여 만드는 경우

m * m * m( ... n번)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;
 
int result[7];
bool visited[7];
int casecount = 0;
 
void make_game(int idx){
    if (idx > 6) {
        casecount++;
        for (int i = 1; i <= 6; i++){
            cout << result[i] << " ";
        }
        cout << endl;
        return
    }
    for (int j = 1; j <= 3; j++){
        if (!visited[idx]){
            result[idx] = j;
            visited[idx] = true;
            make_game(idx + 1);
            visited[idx] = false;
        }
    }
}
 
int main(){
    make_game(1);
    cout << casecount << endl;
    return 0;
}
 
cs






'알고리즘' 카테고리의 다른 글

증가하는부분수열(Longest Increase Subsequence)  (0) 2015.04.26
퇴각 검색(BackTracking)  (0) 2015.04.22
DP(Dynamic Programming)패턴 연구  (0) 2015.04.03
소수 판별 알고리즘  (0) 2015.03.30
최대공약수 구하기  (0) 2015.03.25
Posted by slender ankles
,

동적 계획법(dp)는 가장 흔한 문제 유형 중 하나이고 메모이제이션은 굉장히 자주 구현하게 된다. 

따라서 한 가지 패턴을 정해두고 풀게 되면 확실한 풀이 방법을 알 수 있을 것 같다. 

1. 가장 먼저 basecase 처리

2. cache[]를 모두 -1로 초기화하고, -1이 아니면 cache[]의 값이 없는 것으로 간주.

3. ret(리턴 값)을 참조형(reference)으로 관리하여 실수를 줄임

4. memset을 이용한 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int cache[2500][2500];
 
int dp(int a, int b){
    // 1. base case 처리
    if(...) return ...;
    // (a, b)에 대한 답을 구한 적이 있으면 바로 반환
    int & ret = cache[a][b];  // 3. 참조형(int &)을 이용하여 ret 관리
    if(ret != -1return ret; // 2. -1이 아니면 값이 없는 것으로 간주하여 리턴
    // 답 계산
    ...
    return ret;
}
 
int main(){
    // 4. memset으로 초기화
    memset(cache, -1sizeof(cache)); 
}
 
cs




Posted by slender ankles
,

- 우선 2를 제외한 모든 짝수들은 소수가 아니다라는 점

- 만약 N이 합성수라면 N = a * b형태이므로(a > 1 && b > 1), a와 b 둘 중 하나는 sqrt(N)보다 작거나 같음을 알 수 있다.

그러므로 N을 2부터 sqrt(N)까지 나눠보아 나누어지지 않으면 N은 소수가 된다. 


다음은 2부터 N까지 소수를 구하여 prime이라는 배열에 반영하는 코드의 핵심 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void check_prime(int number){
    for (int i = 2; i <= number; i++){
        if (i % == && i != 2){
            prime[i] = 0;
            continue;
        }
        bool isPrime = true;
        for (int j = 2; j*<= i; j++){
            if (i % j == 0){
                isPrime = false;
                break;
            }
        }
        if (isPrime) prime[i] = 1;
    }
}
 
cs


Posted by slender ankles
,

gcd는 

예를 들어 

8, 28의 최대 공약수를 구해보면


3 = 28 / 8 ...4

2 = 8 / 4 ...0




AB/GCD = GCD*a*b;

1
2
3
4
5
6
7
8
gcd(int a, int b){
 if(b == 0){
     return a;
  }
 else
     return gcd(b, a%b);
}
 
cs

결국 AB/GCD는 최소공배수가 된다

Posted by slender ankles
,

한 붓 그리기에 대한 공식을 명확히 알 필요가 있을 것 같아 정리해둘려고 한다. 

한 붓 그리기란 그래프의 어느 한 점에서 시작하여 모든 간선들을 한 번씩만 지나가는 

경우를 말한다. 

이 것에 대한 조건은 일단 두 가지로 나뉜다. 

1) 모든 정점이 가지는 간선이 다 짝수 개 일 때



2) 두 정점만 간선이 홀 수개를 가질 때 



이렇게 가능하다고 한다. 

한붓그리기의 경로를 뽑아내기 위해서는 dfs의 마지막 부분에 출력을 넣어주면 된다. 

뒤에서부터 출력하면 가능하다. 이 부분은 제대로 이해를 못 해서 직접 손으로 그려봐야겠다


특징

- 오일러 서킷

1) 모든 정점의 간선의 개수는 짝수여야 함(무향)

2) 들어오고 나가는 것이 있어야 됨(유향)


- 오일러 트레일

시작점과 끝점의 간선이 홀수개여야 함(무향)

(방향)

시작점 : 들어오는 간선이 나가는 것보다 하나 더 적어야 함

끝 점 : 들어오는 간선이 나가는 것보다 하나 많아야 한다.

'알고리즘' 카테고리의 다른 글

퇴각 검색(BackTracking)  (0) 2015.04.22
완전탐색 경우의 수를 재귀로 수행하는 방법  (0) 2015.04.15
DP(Dynamic Programming)패턴 연구  (0) 2015.04.03
소수 판별 알고리즘  (0) 2015.03.30
최대공약수 구하기  (0) 2015.03.25
Posted by slender ankles
,