편집 거리 알고리즘이란, 두 문자열의 유사도를 판단하는 알고리즘이다.


유사도를 판단하는 기준은, 어떠한 문자열을 삽입,삭제,변경을 몇 번이나 해서 바꿀수 있는지를 계산하여


그 최소값을 구해, 그 값을 유사도 판단의 척도로 다룬다.



간단하게 예를 들어보자. 여기선 LCS와 마찬가지로 2차원 배열을 통해 문자열을 하나 하나씩 비교를 해나간다.


문자열 MICROSOFT와 NCSOFT를 비교한다고 하자. 모든 연속 부분 집합에 대해서 비교를 해야한다.


먼저 두 문자열의 처음 비교 대상은 공집합 두개이다. 둘 다 같은 문자열이기때문에 바꿀 것이 없어 코스트는 0이다.


그 다음은 N과 {}이다 m이 {}이 되려면 하나를 추가 해야한다. 따라서 코스트는 1이다.

이어서 NC와 {}이다. 두개를 추가해야되기 때문에 코스트는 2가된다.

이런 식으로 먼저 테이블을 그리면 다음과 같은 형태를 가진다.


 

 {}

 N

 C

 S

 O

 F

 T

 {}

 0

 1

 2

 3

 4

 5

 6

 M

 1

 

 

 

 

 

 

 I

 2

 

 

 

 

 

 

 C

 3

 

 

 

 

 

 

 R

 4

 

 

 

 

 

 

 O

 5

 

 

 

 

 

 

 S

 6

 

 

 

 

 

 

 O

 7

 

 

 

 

 

 

 F

 8

 

 

 

 

 

 

 T

 9

 

 

 

 

 

 



그 다음 M과 N을 비교해보자. 이 둘은 다르다. 따라서 값을 변경해야 한다. 코스트는 1이다.

이어서 M과 NC이다. 둘 다 다르고, 하나를 지우고 하나를 바꿔야한다.  따라서 코스트는 2이다.

M과 NCS를 비교하자. 셋 다 다르고 두개를 지우고 하나를 바꿔야한다. 따라서 코스트는 3이다.

이런식으로 세 번째 줄 까지만 채워보면 밑과 같다.


 

 {}

 N

 C

 S

 O

 F

 T

 {}

 0

1

2

3

4

5

6

 M

 1

1

2

3

4

5

6

 I

 2

2

2

3

4

5

6

 C

 3

3

2

3

4

5

6

 R

 4

 

 

 

 

 

 

 O

 5

 

 

 

 

 

 

 S

 6

 

 

 

 

 

 

 O

 7

 

 

 

 

 

 

 F

 8

 

 

 

 

 

 

 T

 9

 

 

 

 

 

 



테이블을 직접 그리다보면 다음과 같은 사실을 깨달을 수 있다.

문자열 M과 문자열 NC를 비교한다고 해보자. 여기서 문자열 M과N은 이미 순차적으로 비교를 한 상태이다. 이어서 문자열 M과 C이 만약 같다고 할 때, 이것에 대한 코스트는 바꿔즐 필요가 없다. 따라서 M과 N을 비교했던 값을 가져오기만 하면 된다.

그리고 만약 M과 C가 다르다고 했을 경우엔, M과 N을 비교했던 값에서 코스트를 하나 증가시켜주면 된다.


이 방법을 좀 더 긴 문자열에 적용해보자.

MICROSOFT와 NCSOFT 마지막 T끼리 비교했을 때, T는 서로 일치하므로 그 전까지 비교했던 MICROSOF와 NCSOF의 편집거리를 가져오면 된다.

MICRO와 NCS를 비교 할 경우, O와 S는 서로 다르므로, MICR과 NC를 비교했을때의 편집거리에다가 1을 증가시키면 된다.


여기서 첫 번째, 두 변수가 일치했을 경우 이전의 편집거리를 가져오면 된다고 했다. 

따라서 A[i] == B[j]일때의 편집거리 D(i,j) = D(i-1,j-1)이다.


그리고 두 문자가 다를 경우, 이전의 편집거리에서 1을 증가시켜야 한다고 했다.

MI와 NCS를 비교한다고 해보자. (위 테이블에서 이 경우 비교하는 대상은 I와 S이다. 이를 기억하고 보자.) 

왼쪽 대각선 위에 있는건 M와 NC를 비교하는 편집거리이다. 위는 M과 NCS를 비교하는 편집 거리이며, 왼쪽은 MI와 NC를 비교하는 편집 거리이다.


1. 여기서 대각선 M과 NC의 편집거리를 가져오는 것은 I와 S를 같은 문자로, 두 문자열이 같다고 보는 것이다. 즉 I를 S로 바꾸면 비교하는 문자가 같으니 M과 NC를 비교한 편집거리를 그대로 가져오고, I를 S로 바꾼 수정을 위한 코스트를 더하는 것이다. (위에서 같을 경우엔 대각선위에 있는 값을 그대로 가져왔었던걸 떠올리면 된다.)


2. 위쪽인 M과 NCS의 편집거리에서 1을 증가시켜 MI,NCS 편집거리에 넣는다는 것은 I를 삭제한다는 것을 뜻한다. 즉 M과 NCS의 편집거리를 그대로 가져오고 I를 삭제한 코스트를 추가한다는 뜻이다.


3. 마지막으로 왼쪽인 MI와 NC의 편집거거에서 1을 증가시켜 값을 MI,NCS 편집거리에 넣는다는 것은.  MI 뒤에 문자를 하나 추가한다는 것이다. 즉 MI뒤에 문자열을 하나 추가시켜 MIS,NCS이므로 MI,NC의 편집거리에다 추가시킨 비용을 더한 값을 넣는 것이다.




<그림으로 좀 더 자세히 이해해 보자>



즉 A[i] != B[j]면, 1,2,3번, 각 수정,삽입,삭제를 한 편집거리 중 최소값을 가져오면 된다. 즉 D(i,j) = min(D(i-1,j)+1,D(i,j-1)+1,D(i-1,j-1)+1)이다.



즉 정리하면 다음과 같다.

1. 비교하고자 하는 문자 A[i]와 B[j]가 같으면, D(i,j) = D(i-1,j-1)이다.

2. 비교하고자 하는 문자 A[i]와 B[j]가 다르면, D(i,j) = min(D(i-1,j)+1,D(i,j-1)+1,D(i-1,j-1)+1)이다.


위 규칙 대로 테이블을 채우면 다음과 같다. 규칙대로 한번 채워보고, 직접 문자열을 비교해서 채워서 두 테이블을 비교해보면 확실하게 알 수 있다.



 

 {}

 N

 C

 S

 O

 F

 T

 {}

 0

1

2

3

4

5

6

 M

 1

1

2

3

4

5

6

 I

 2

2

2

3

4

5

6

 C

 3

3

2

3

4

5

6

 R

 4

4

3

3

4

5

6

 O

 5

5

4

4

3

4

5

 S

 6

6

5

4

4

4

5

 O

 7

7

6

5

4

5

5

 F

 8

8

7

6

5

4

5

 T

 9

9

8

7

6

5

4



여기서 맨 마지막 값이 편집거리가 된다.


이를 코드로 옮기면 다음과 같다.



C++ 편집 거리 알고리즘

#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;

string input1,input2;
int dist[1001][1001];

int levenshtein(string& input1, string& input2){
    for(int i=1; i<=input1.length(); i++)
        dist[i][0] = i;
    for(int j=1; j<=input2.length(); j++)
        dist[0][j] = j;
        
    for(int j=1; j<=input2.length(); j++){
        for(int i=1; i<=input1.length(); i++){
            if(input1[i-1] == input2[j-1]) dist[i][j] = dist[i-1][j-1];
            else dist[i][j] = min(dist[i-1][j-1]+1,min(dist[i][j-1]+1,dist[i-1][j]+1));
        }
    }   
    for(int j=0; j<=input2.length();j++){
        for(int i=0;i<=input1.length();i++)
            printf("%d\t",dist[i][j]);
            printf("\n");
    }
    return dist[input1.length()][input2.length()];
}

int main(){
    cin>>input1>>input2;
    cout<<"편집 거리 : "<<levenshtein(input1,input2)<<endl;
}

코드를 작성하고 보니 LCS 알고리즘이랑 유사점이 많아보인다는 생각이 들었다.


<실행 화면>






외판원 문제는 NP문제로 유명한 문제이다. 


여러 도시들이 주어져 있고, 모든 도시들에 대한 가중치가 주어졌을때, 단일 시작점부터 시작해서 모든 도시를 단 한번씩만 방문하여 다시 시작점으로 돌아오는데 드는 최단 거리를 구하는 문제이며, 말은 그냥 일반 그래프 문제인거 같아 그리 어려워 보이지 않지만, 무려 NP-hard 문제에 속한다.


이 문제가 어려운게 완전 연결 그래프라는 점인데, 단순히 완전 탐색으로 진행을 하면 무려 O(N!)라는 시간복잡도가 나온다.


이는 10개만 탐색해도 3,628,800이라는 값이 나오며,  20개는 2,432,902,008,176,640,000개라는 상상하기도 힘든 값이 나온다. 

완전 탐색으로도 풀이는 가능하지만 왠만하면 지양해야 하는 방법이다.

DP로도 생각보다 빠른 시간으로 탐색이 안되서, 실제로는 그리디로 '최단 거리'는 못구해도 '그나마 괜찮은 거리'를 구하는 정도로 사용하는 듯 하다.



<C++ 완전 탐색 TSP>

0번 도시부터 시작해서 모든 도시를 순회한 후 최종적으로 다시 0번으로 돌아오는 코드이다.

무식하게 모든 경우의 수를 일일히 다 계산하는 방법이다.

#include <iostream> #include <vector> #include <cmath> #include <algorithm> using namespace std; int n; //가중치를 저장하기 위핸 배열 int dist[15][15]; int TSP(vector<int> path, vector<bool> visited, int len){ //모든 도시 다 방문했을 경우 if(path.size() == n) return len+dist[path.back()][0]; int ret = 987654321; for(int next=0; next<n;next++){ //방문 했다면 패스 if(visited[next]==true) continue; int cur = path.back(); path.push_back(next); visited[next] = true; ret = min(ret,TSP(path,visited,len+dist[cur][next])); visited[next] = false; path.pop_back(); } return ret; } int main(){ cin >> n; for (int i = 0; i < n; i++){ for (int j = 0; j < n; j++){ cin >> dist[i][j]; } } vector<int> path(1, 0); // 경로를 저장할 벡터, 시작 도시 0번도시 선택. vector<bool> visited(n, false); // 방문 여부를 저장할 벡터. false로 초기화. visited[0] = true; // 출발 도시 방문여부 체크. double ret = TSP(path, visited, 0); cout << ret << endl; }


하지만 앞서 말했듯이 이 코드는 15개 이상부터는 어마어마한 시간이 걸린다.


그래서 완전 탐색 말고 DP를 사용하는 방법을 보도록 하자.




<C++ TSP 동적계획법>


동적계획법으로 구현하려면 이를 부분 문제로 쪼개야 한다.

완전 탐색으로 구현했을 때 쓰는 값들은 지금까지의 경로를 저장한 벡터, 방문여부를 저장한 벡터, 지금까지 연결된 거리이다.

동적계획법은 기본적으로 각각 독립적인 부분 문제로 쪼개지므로 지금까지 연결된 거리는 필요가 없고, 현재점을 기준으로 연산을 해야한다.


즉 


라는 점화식으로 풀 수 있다.(여기서 n은 0부터 next까지로, visited를 검사해서 방문한 적이 없는 값으로만 조사를 진행해야된다.)


시작점과, 현재 방문한 도시를 기준으로 모든 도시를 순회해서 얻을 수 있는 최소 거리를 출력하는 식이다.


그럼 거리에 대한 정보를 어떻게 메모이제이션을 할까? 해당 시작점으로 출발하는 경로는 메모이제이션 할때 어떻게 표현할까?


이에 대한 답은 비트마스킹이다. 비트로 도시의 방문 여부를 체크하는 것이다.

예를 들어 0번 도시와 2번 도시를 방문했다면, 00000101 = 3으로 체크가 될 것이고,  0번,3번,5번 도시를 방문했다면 00101001 = 41이 될것이다.

즉 1번 건물에서 1,2,3번 건물을 방문한 것은 cache[1][7]에 메모이제이션 하면 될 것이다.


int형은 4바이트, 총 32비트이기 때문에 int형 변수만으로도 32개 도시를 표현 할 수 있다.


코드를 작성하기전에 비트 연산을 간단하게 보도록 하자.

1<<n : 1을 n만큼 왼쪽으로 시프트한다. 즉 1<<4이면 10000이다.

a = a | (1<<n) : a라는 변수의 n번째 비트를 켠다.

a & (1<<n) : a라는 변수의 n번째 비트가 켜있으면 1<<n을, 꺼있으면 0을 반환한다.

a -=(1<<n) : a라는 변수의 n번째 비트를 끈다(1->0). 단 무조건 켜져 있을 때만 사용해야한다.

a &=~(1<<n) : a라는 변수의 n번째 비트를 끈다. 꺼져 있으면 꺼진 상태로 유지한다.

a ^=(1<<n) : a변수의 n번째 비트를 토글한다. 켜있으면 끄고, 꺼있으면 킨다.


즉 위 비트연산을 이용하면 visited&(1 << next)는 next번째 도시의 방문 여부를 확인하는 것이고.

또한 재귀 호출의 visited인자로 visited | (1 << next)를 전달 하는 것은, 해당 도시의 방문 여부를 체크해서 전달하는 것이다.


밑은 비트마스킹 DP를 이용해 구현한 코드이다.

#include <cstdio> #include <cmath> #include <algorithm> #define MAX_VALUE 987654321.0 using namespace std; int n; //경로를 저장할 dp int cache[17][65536], dist[17][17]; int TSP(int cur, int visited) { //점이 10개라면, 100000000000-1 =011111111111; if (visited == (1 << n)-1) return dist[cur][0]; int& ret = cache[cur][visited]; if (ret != 0) return ret; ret = MAX_VALUE; for (int next = 0; next <= n; next++) { if (visited&(1 << next))continue; if (dist[cur][next]==0) continue; ret = min(ret,TSP(next, visited | (1 << next)) + dist[cur][next]); } return ret; } int main(){ scanf("%d", &n); for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ scanf("%d",&dist[i][j]); } } printf("%d",TSP(0,1)); }


완전 탐색으로 푸는 TSP는 O(N!)인것에 반해 DP는 O(2^N*N^2)이다. DP로 풀어도 빠른 속도는 아니지만 완전 탐색에 비해 현저하게 빨라지긴 했다.



<경로 추적하기>


위 소스 코드에서 메모이제이션한 값으로 경로를 추적해보았다.

방법은 간단하다. 이 소스코드는 0이라는 위치부터 시작하므로,

 전체 거리 - 0부터 다른 경로까지의 거리 = cache[k][masking + (1 << k)](k에서 masking에 켜져있는 비트의 도시들과 k번째 도시를 방문한 값)을 만족하는 것을 찾으면, k가 바로 다음 경로가 되는 것이다.

이 k를 경로를 저장하기 위한 벡터에 저장하자.  그 후k를 다음 경로를 찾기 위해 비교하기 위한 변수 piv에 넣는다.

그리고 비교할 거리를 masking에 켜진 도시들과 k를 방문했으며, k부터 시작하는 거리인 cache[k][masking + (1 << k)]으로 바꾼 후, k번째 비트를 방문했다는 표시로 켜준뒤 전과 같은 연산을 반복적으로 진행하면 된다.


void printPath(long double distance){ int piv = 0, masking = 1; //cache 배열을 탐색해가며 다음 경로를 찾는다. for(int j = 0; j<=n;j++){ for(int k = 0; k <= n; k++){     if(masking&(1 << k)) continue;      if (distance - dist[piv][k] == cache[k][masking + (1 << k)]) {     //다음 경로 저장      path.push_back(k);     distance = cache[k][masking + (1 << k)];      piv = k; masking += (1 << k); } } } //경로 출력 for(int i=0; i<path.size();i++) printf("(%d)->",path[i]); printf("(0)"); }



[문제 링크]



LCS2는 다음 포스트에 있는 내용과 같다.


http://hsp1116.tistory.com/37



#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
vector<char> output;
int cache[1001][1001];
char input[1001],compare[1000];
int LCS(int m,int n){
    memset(cache,0,sizeof(cache));
    for(int i=1;i<=m;i++){
        for(int j=1; j<=n;j++){
            if(compare[i-1] == input[j-1]){
                cache[i][j] = cache[i-1][j-1] +1;
                }
            else{
                cache[i][j] = std::max(cache[i-1][j], cache[i][j-1]);
            }
        }
    }
    return cache[m][n];
}
void backTracking(int m, int n){
    if(m==0 || n ==0) return;
    if(cache[m][n] > cache[m-1][n-1] && cache[m][n] > cache[m][n-1] && cache[m][n] > cache[m-1][n]){
        output.push_back(input[n-1]);
        backTracking(m-1, n-1);
    }else if(cache[m][n] > cache[m-1][n] && cache[m][n] == cache[m][n-1]){
        backTracking(m, n-1);
    }else{
          backTracking(m-1, n);
    }
}
int main(){
    scanf("%s%s",input,compare);
    int m = strlen(compare), n = strlen(input);
    printf("%d\n",LCS(m,n));
    backTracking(m,n);
    for(int i=output.size()-1;i>=0;i--)
        printf("%c",output[i]);
}


'Algorithm > Problems' 카테고리의 다른 글

백준 - 1238 파티  (0) 2016.04.22
백준 - 1753 최단경로  (1) 2016.04.22
백준 - 9251 LCS  (0) 2016.04.17
알고스팟 - KOOGLE  (0) 2016.04.14
백준 - 1725, 6549 히스토그램 / 알고스팟 - FENCE  (0) 2016.03.28


[문제 링크]



LCS는 다음 포스트에 있는 내용과 같다.


http://hsp1116.tistory.com/37




#include <cstdio>
#include <algorithm>
#include <cstring>
int cache[1001][1001];
char input[1001],compare[1000];
int LCS(){
    int n = strlen(compare), m = strlen(input);
    memset(cache,0,sizeof(cache));
    for(int i=1;i<=n;i++){
        for(int j=1; j<=m;j++){
            if(compare[i-1] == input[j-1]){
                cache[i][j] = cache[i-1][j-1] +1;
                }
            else{
                cache[i][j] = std::max(cache[i-1][j], cache[i][j-1]);
            }
        }
    }
    return cache[n][m];
}
int main(){
    scanf("%s %s",input,compare);
    printf("%d\n",LCS());
}

'Algorithm > Problems' 카테고리의 다른 글

백준 - 1753 최단경로  (1) 2016.04.22
백준 - 9252 LCS 2  (0) 2016.04.17
알고스팟 - KOOGLE  (0) 2016.04.14
백준 - 1725, 6549 히스토그램 / 알고스팟 - FENCE  (0) 2016.03.28
백준 - 2493 탑  (1) 2016.03.28

공통 부분 수열이란, 두 문자열이 공통으로 가지고 있는 부분 수열을 말한다.


예를 들어 문자열


문자열 A : CDABE

문자열 B : CDEGT

가 있다면,


공통 부분 수열은

{},{C},{D},{E},{C,D},{D,E},{C,E},{C,D,E}

일 것이다.





1. 최장 공통 부분 수열 길이 구하기

최장 공통 부분 수열은 공통 부분 수열 중에서 길이가 가장 긴 부분 수열을 말한다.

위 예제 최장 공통 부분 수열은{C,D,E}이고, 길이가 3이다.


최장 증가 부분 수열은 또한 다음과 같은 정의를 따른다. 각 점화식을 에제를 들어 확인해보자.





(1) Ai != Bj

두 문자열 CDABE,CDEGT의 최장 공통 부분 수열을 구한다고 해보자.

여기서 맨 마지막 문자인 E와 T가 각각 다르므로 다음과 같은 경우를 고려 할 수 있다.

1. 최장공통부분 수열이 E로 끝나는 경우.

이 경우 K는 최장 공통 부분 수열에 아무련 영향을 끼치지 않으므로 없어도 상관 없다.

즉 LCS(i,j) = LCS(i,j-1)이다.


2. 최장 공통 부분 수열이 T로 끝나는경우

이 경우 E가 최장 공통 부분 수열에 아무런 영향을 끼치지 않으므로 지워도 상관없다.

이 경우엔 LCXS(i,j) = LCS(i-1,j)이다 


3. 둘 다 해당이 되지 않는 경우.

둘 다 지워도, 하나만 지워도 최장 증가 부분 수열에 영향을 끼치지 않는다. 따라서 고려 할 필요가 없다.


두 문자가 일치하지 않을 때, 최소 둘 중 하나는 최장 공통 부분 수열에 영향을 끼치지 않으므로 둘 중 하나를 지워도 상관없다.

즉 둘 중 하나를 지웠을 때의 더 큰 LCS가 LCS(i,j)이다.

따라서 LCX(i,j) = max(LCS(i-1,j),LCS(i,j-1))임을 알 수 있다.



(2) Ai == Bj

두 문자열 CDABE CDE라고 가정해보자. 맨 뒤의 문자 E가 설 일치하는데, 공통 문자인 E는 최장 공통 부분 수열에 반드시 포함될 것이다.

따라서 이 두 문자열을 지운다면, 최장 공통 부분 수열에서 또한 E가 사라질 것이라는 것을 알 수 있다.

즉 최장 증가 부분 수열의 길이 또한 1 감소하므로, LCX(i,j) = LCS(i-1,j-1) + 1이다.





이제 위 공식을 이용하여 구현하면 된다. 각 인덱스는 현재 문자열의 길이라고 생각하면 된다.


 

 

 C (j=1)

 D (j=2)

 E (j=3)

 G (j=4)

 T (j=5)

 

 0

 0

 0

 0

 0

 0

 C (i=1)

 0

 

 

 

 

 

 D (i=2)

 0

 

 

 

 

 

 A (i=3)

 0

 

 

 

 

 

 B (i=4)

 0

 

 

 

 

 

 E (i=5)

 0

 

 

 

 




맨첨 1행, 1열들은 모두 0으로 치워지는데 이는 빈 수열을 뜻한다. 빈 수열과 문자열의 최장 증가 부분 수열은 0이기 때문이다.



밑은 앞서 확인한 공식으로 작성한 테이블이다.


 

 

 C (j=1)

 D (j=2)

 E (j=3)

 G (j=4)

 T (j=5)

 

 0

 0

 0

 0

 0

 0

 C (i=1)

 0

 1

 1

 1

 1

 1

 D (i=2)

 0

 1

 2

 2

 2

 2

 A (i=3)

 0

 1

 2

 2

 2

 2

 B (i=4)

 0

 1

 2

 2

 2

 2

 E (i=5)

 0

 1

 2

3

 3

 3


LCS(5,5)=3으로, 최장 공통 부분 수열의 길이는 3임을 알 수 있다.


int LCS(string& input, string& compare){
    int cache[1001][1001];
    memset(cache,0,sizeof(cache));
    for(int i=1;i<=compare.length();i++){
        for(int j=1; j<=input.length();j++){
            if(compare[i-1] == input[j-1]){
                cache[i][j] = cache[i-1][j-1] +1;
                }
            else{
                cache[i][j] = max(cache[i-1][j], cache[i][j-1]);
            }
        }
    }
    return cache[compare.length()][input.length()];
}



1. 최장 공통 부분 수열 구하기


이는 우리가 구한 2차원 배열을 역추적 하여 구할 수 있다.


 

 

 C (j=1)

 D (j=2)

 E (j=3)

 G (j=4)

 T (j=5)

 

 0

 0

 0

 0

 0

 0

 C (i=1)

 0

 1

 1

 1

 1

 1

 D (i=2)

 0

 1

 2

 2

 2

 2

 A (i=3)

 0

 1

 2

 2

 2

 2

 B (i=4)

 0

 1

 2

 2

 2

 2

 E (i=5)

 0

 1

 2

3

 3

 3



최장 증가 부분 수열을 찾는 공식을 잘 고려해 보자.


우리가 찾아야 하는건 Ai == Bi인 문자이다.


Ai == Bj일 때 LCS(i,j) = LCS(i-1,j-1) + 1였는데. 이를 고려해 역추적 해나가면 된다.

다만 또 고려할 것이 있는데, LCX(i,j) = max(LCS(i-1,j),LCS(i,j-1))의 경우이다.

Ai != Bj일 지라도, LCS(i,j) = max(LCS(i-1,j),LCS(i,j-1))를 통해 LCS(i-1,j-1) + 1가 도출 될 수 있다.

따라서 LCS(i-1,j), LCS(i,j-1)이 둘 다 LCS(i,j)보다 작으면서, LCS(i-1,j-1)이 LCS(i,j)보다 작은 경우가 Ai==Bj일 때이다. 

즉 LCS[i][j] > LCS[i-1][j-1] && LCS[i][j] > LCS[i][j-1] && LCS[i][j] > LCS[i-1][j])를 조건으로 코드를 구현하면 된다.


밑은 해당 코드이며, output에 최장 공통 부분 수열 문자열이 저장된다.


string output;
void backTracking(int m, int n){
    if(m==0 || n ==0) return;
    if(cache[m][n] > cache[m-1][n-1] && cache[m][n] > cache[m][n-1] && cache[m][n] > cache[m-1][n]){
        //문자열 인덱스는 캐시 인덱스보다 1씩 더 작다. 
        output = input[n-1] + output;
        backTracking(m-1, n-1);
    }else if(cache[m][n] > cache[m-1][n] && cache[m][n] == cache[m][n-1]){
        backTracking(m, n-1);
    }else{
          backTracking(m-1, n);
    }
}



[문제 링크]



문제 자체는 입력을 문자열로 받아 문자의 아스키코드로 문자,숫자만 체크하면 되는 간단한 문제다.


다만 이 문제에서 주의해야할 점은 입력받는 문자열의 길이가 최대 1000개라는 점이다


문제를 제대로 읽지 않고 그냥 제곱연산을 했다가 오답이 떴는데 Strength(x) = 26^A * 10^B이므로 숫자로만 1000개 채워졌다고 해도, 10^1000이다.


크게 봐줘서 64비트 정수형을 쓴다고 해도 2^64-1은 아득히 넘어버린다.(Big Integer 라이브러리를 써도 안될거같다.)


그래서 이 문제는 그냥 pow로 구하기엔 자료형 범위를 초과하는 값은 정확한 비교가 되질 않는다.


하지만 이 문제는 암호의 강도가 정확히 몇인지를 알아야 하는 문제가 아니고, 뭐가 더 강한지만 고려하면 되기 때문에 그냥 log를 씌워서 계산한 후 비교하면 된다.




#include <iostream>
#include <cmath>
#include <cstring>
#include <string>
using namespace std;
int testcase,n;
string str;
long double Strength(const string& str){
    int num=0,chr=0;
    for(int i = 0; i<str.length(); i++){
        if(str[i]>=97 && str[i]<=122) chr++;
        else num++; 
    }
    return chr*log(26) + num*log(10);
}
int main(){
    cin>>testcase;
    while(testcase--){
        string maxStr;
        long double maxValue = 0;
        cin>>n;
        for(int i = 0; i<n; i++){
            cin>>str;
            long double cur = Strength(str);
            if(cur > maxValue){
                maxStr = str;
                maxValue = cur;
            }
            else if(cur == maxValue && maxStr.compare(str)>0) maxStr = str;
        }
        cout << maxStr << endl;
    }
}


위도 accept는 되지만 밑은 메모리와 시간을 좀 더 줄인 코드이다. 


#include <cstdio> #include <cmath> #include <cstring> int testcase,n; char str[1001]; long double Strength(char* str){ int num=0,chr=0; for(int i = 0; i<strlen(str); i++){ if(str[i]>=97 && str[i]<=122) chr++; else num++; } return chr*log(26) + num*log(10); } int main(){ scanf("%d",&testcase); while(testcase--){ char maxStr[1001]; long double maxValue = 0; scanf("%d",&n);getchar(); for(int i = 0; i<n; i++){ scanf("%s",str); long double cur = Strength(str); if(cur > maxValue){ strcpy(maxStr,str); maxValue = cur; }else if(cur == maxValue &&strcmp(maxStr,str)>0)strcpy(maxStr,str); } printf("%s\n",maxStr); } }


'Algorithm > Problems' 카테고리의 다른 글

백준 - 9252 LCS 2  (0) 2016.04.17
백준 - 9251 LCS  (0) 2016.04.17
백준 - 1725, 6549 히스토그램 / 알고스팟 - FENCE  (0) 2016.03.28
백준 - 2493 탑  (1) 2016.03.28
백준 - 10799 쇠막대기  (2) 2016.03.28


해쉬란?


해쉬는 임의의 크기를 가진 데이터를 고정된 데이터의 크기로 변환시키는 것을 말한다.

즉 해쉬 알고리즘은 해쉬를 하는 방법에 대해 절차적으로 명세한다.


이를 이용해 특정한 배열의 인덱스나 위치나 위치를 입력하고자 하는 데이터의 값을 이용해 저장하거나 찾을 수 있다.

기존에 사용했던 자료 구조들은 탐색이나 삽입에 선형시간이 걸리기도 했던것에 비해, 

해쉬를 이용하면 즉시 저장하거나 찾고자 하는 위치를 참조할 수 있으므로 더욱 빠른 속도로 처리할 수 있다.




1. Direct Addressing Table






Direct Addressing Table은 key-value쌍의 데이터를 배열에 저장할 , key값을 직접적으로 배열의 인덱스로 사용하는 방법이다.

예를 들면 키 값이 400인 데이터가 있다면, 이는 배열의 인덱스가 400인 위치에 키 값을 저장하고 포인터로 데이터를 연결한다.

똑같은 키 값이 존재하지 않는다고 가정하면, 삽입 시에는, 각 키마다 자신의 공간이 존재하므로 그 위치에다 저장을 하면 되고, 

삭제 시에는 해당 키의 위치에 NULL값을 넣어주면 된다.탐 색 시에는 해당 키의 위치를 그냥 찾아가서 참조하면 된다.

찾고자 하는 데이터의 key만 알고있으면 즉시 위치를  찾는 것이 가능하므로 탐색,저장,삭제,갱신은 모두 선형시간인 O(1)로 매우 빠른 속도로 처리가 가능하다.

다만 key값의 최대 크기만큼 배열이 할당 되기 때문에, 크기는 매우 큰데, 저장하고자 하는 데이터가 적다면 공간을 많이 낭비할 수 있다는 단점이 있다.





2. Hash Table




Hash Table은 key-value 쌍에서 key값을 테이블에 저장할 때, Direct Addressing Table 달리 key값을 함수를 이용해 계산을 수행 한 후,

그 결과값을 배열의 인덱스로 사용하여 저장하는 방식이다. 여기서 key값을 계산 하는 함수는 해쉬 함수(Hash Function)이라고 부르며,

해쉬 함수는 입력으로 key를 받아, 0부터 배열의크기-1 사이의 값을 출력한다. 해쉬에 대한 첫 정의대로 임의의 숫자를 배열의 크기 만큼으로

변환 시킨 것이다.

이 경우 k값이 h(k)로 해쉬되었다고 하며, h(k)는 k의 해쉬값이라고 한다.


위 그림을 참조하면 각 k값들의 해쉬값인 h(k)값들이 배열의 인덱스로 사용됨을 확인 할 수 있다. 

해쉬 테이블은 Direct Addressing Table에 비해 공간 낭비가 매우 적은데 이는 key값의 크기에 테이블의 크기가 좌우되는 것이 아니고,

h(k)만큼의 공간에 저장되기 때문이다. 



2.1 충돌(Collusion)


하지만 해쉬 테이블은 '충돌'이 일어 날 수 있다는 큰 문제점이 있다. 충돌이란, 다른 k값이 동일한 h(k)값을 가져 동일한 slot에 저장되는 경우를 말한다.

예를 들자면 k1과 k12을 해쉬하였더니 h(k1) = h(k12)인 경우를 들 수 있다. Direct Addressing Table에서는 이를 방지 하기 위해 모든 key값이 다르다고 전제하였지만

해쉬 테이블에서는 key값이 달라도 해쉬의 결과가 같을 수 있기 때문에 이를 방지하기 위한 방법이 필요하다.

하지만 해쉬 함수를 짜더라도 충돌을 '완전히' 방지한다는 것을 보장하기는 힘드므로, 충돌을 방지하기 위한 방법으로

충돌을 어느정도 허용하되 이를 최소화 하는 방법도 사용하기도 한다..



2.1.1 Chaining 방법 - 충돌을 허용하되 최소화 하는 방법





충돌을 허용하지만 이를 최소화 하기 위한 방법중 하나인 체이닝 방식이다. 체이닝이란 이름 그대로 데이터들을 포인터를 이용해 서로 체인 형태로 엮어 나가는 것을 뜻하며, 해쉬 테이블에선 동일한 해쉬값이 출력되 충돌이 일어나면, 그 위치에 있던 데이터에 key값을 포인터로 뒤이어 연결한다.

따라서 최초로 h(k)위치에 저장된 데이터를 시작으로 그 이후의 h(k)값이 출력되는 데이터는 모두 연결 리스트의 형태를 취한다.

그렇기 때문에 최초의 위치를 탐색하는 해쉬 과정을 제외하고, 모든 탐색, 삽입, 삭제 과정은 연결리스트와 유사한 방식으로 진행한다. 


Chaing 방법에서의 수행시간은 삽입 시에는 해쉬값을 이용해 바로 slot에 저장하면 되므로 상수시간에 일어나고, 삭제는 연결리스트의 삭제와 동일하게 상수시간에, 탐색 시에는 연결리스트를 따라 가기 때문에 리스트의 길이 만큼 발생하지만, 최악의 경우, 즉 모든 데이터의 해쉬값이 일치하여 한 인덱스에 저장됬을 경우엔 연결리스트의 탐색 시간과 동일한 선형시간을 가지게 된다.


2.1.2 적재률


하지만 이 최악의 경우는 극단적인 예료, 평균적인 경우엔 O(a+1)의 시간이 걸린다. a는 적재율을 뜻하며 적재율이란 현재 저장된 key값 갯수(K), 전체 테이블의 갯수(N)을 서로 나눈 값(K/N)이다. 즉 현재 저장된 데이터가 많으면 많아질수록 충돌 확률이 높아져 연결 리스트 탐색 확률도 증가하며, 적을수록 리스트 탐색 확률이 적어진다는 것이다.




2.1.3 Simple uniform hash


충돌을 최소화 하는 방법 중에 충돌이 적은 좋은 해쉬 함수를 만드는 방법도 있다. 좋은 해쉬 함수의 조건은 Simple uniform hash 함수를 만드는 것으로, 이 조건은 다음과 같다.


1. 계산된 해쉬값들은 0부터 배열의 크기-1 사이의 범위를 '동일한 확률'로 골고루 나타날 것.

2. 각각의 해쉬값들은 서로 연관성을 가지지 않고 독립적으로 생성될 것.


첫 번째 조건을 충족하면 충돌이 일어날 확률이 적어질 것이며, 두 번째 조건은 해쉬값들이 서로 연관이 있을 경우 연관성이 있으면 해당 해쉬값이 등장하는 패턴이나 순서가 존재 할 수 있고, 이는 반복적인 충돌을 일으 킬 확률이 있기 때문이다.



2.1.4 divison method


해쉬 함수는 정말 다양하지만 대표적인 해쉬 함수로는 divison method가 있는데, modular 연산 방법을 이용하는 방법이다. 특정 key를 어떤 수로 나눈 나머지를 해쉬값으로 사용한다.

예를 들어 m=100이면 k mod m은 0부터 99까지의 범위를 가진다. 이 범위의 m은 해쉬 테이블의 성능을 크게 좌우하는데, m의 크기는 보통 키의 수의 3배가 적당하다고 한다. (적재율이 30%쯤까지 충돌이 거의 일어나지 않는다고 한다.)

그리고 m으로 2^p값을 사용하는 것엔 큰 주의를 요한다. 왜냐하면 m이 2^3이면, 2진수로 00001000이고, 4번째 이하의 숫자만 해쉬값에 영향을 끼치기 때문이다.

예를 들어 k1과 k2가 각각 10110100,10120100이면 둘 다 같은 해쉬값을 출력한다. 이를 방지하기 위해서 m은 보통 2^p에 근접한 소수를 선택한다고 한다.


즉 가장 최적의 m의 크기는 키의 갯수의 3배이며 2의 지수승에 근접한 소수이다.




3. Open Addressing



Open Addresing은 key값을 테이블에 저장하는 Direct Addressing Table과는 다르게, 모든 데이터(key+데이터)를 테이블에 저장하는 방법이다.

장점으로는 데이터를 직접 모두 읽어 오기 때문에, 포인터를 쓸 일이 없어 포인터를 사용함으로서 발생 할 수 있는 오버헤드를 방지 할 수 있다는 점이다.

포인터가 필요 없어 구현이 훨씬 용이해졌으며, 포인터 접근에 필요한 시간이 없기 때문에 큰 성능 향상이 있다.


3.1.1 Liner probing


포인터를 사용하지 않기 때문에, 앞서 설명한 Chaing 방법은 사용 할 수 없다. 따라서 다른 방법으로 충돌시에 대처해야 하는데 그 중 하나가 Liner probing이다.

Liner probing은 key값으로 인덱스를 계산할 때, 만약 충돌이 발생한다면 바로 다음 인덱스에 데이터를 저장하는 방식이다. 다음으로 이동한 이후에도 충돌이 발생했다면 또 다시 바로 다음 인덱스에 저장한다.

즉 충돌이 일어나지 않을 때 까지 다음 인덱스로 이동을 해가며 빈 공간을 찾으면 그 위치에 저장한다.



위는 하나의 예제이다. m의 크기는 11로 해쉬 함수는 k mod 11로 계산한다.

1. h(54) = 10, h(77) = 0, h(94) = 6, h(89) = 1, h(14) = 3으로 충돌이 일어나지 않는다.

2. h(45) = 1인데, 이미 1의 위치에는 h(89)=1이 저장되어 있어 충돌한다. 따라서 다음 위치에 저장한다

3. h(35) = 2인데, 여기엔 방금 충돌이 일어났던 45가 저장되어 있어 충돌한다, 빈 위치가 저장할 때 까지 이동하여 저장한다.

4. h(76) = 10이며, 저장은 3번과 같이 한다.


매 충돌시마다 한칸씩 이동하므로 해쉬함수는 다으모가 형태를 취하게 된다.


h(k,i) = (k+i) mod m


i는 충돌시마다 증가하여 한칸씩 이동한다.



Liner probing은 정말 구현이 용이하지만, primary clustering이라는 문제점을 가지고 있다. primary clustering은 충돌이 나면, 뒤 슬롯에 데이터를 넣어 하나의 데이터 덩어리를 이루기 때문에, 데이터들의 특정 위치에만 밀집하는 현상을 말한다. 이 현상으로 slot이 많아지면 많아질수록 탐색 시간에 엄청 늘어나게 된다.



3.1.2 Quadratic probing


primary clustering을 방지하기 위해 hash함수를 다음과 같이 2차식의 형태로 만드는 것이다.


h(k,i) = (h'(k) + c1*i + c2*i^2) mod m


Liner probing과는 달리 i가 2차식의 형태를 취해, 한칸씩 이동하는 것이 아닌 c1*i + c2*i^2만큼 이동한다.




위는 간단한 예제이다. 해쉬 함수는 h(k,i) = (k+i^2) mod m 의 형태를 취한다.

3번째에서 h(48,0) = 6으로 기존의 76과 충돌이 일어났다. 그래서 i를 하나 증가시켜 h(48,1) = (48+1^2) mod 7 = 0의 위치에다가 저장하였다. 여기선 충돌이 한번 일어난 경우만 있는 예제이지만. 만약 0에서도 충돌이 일어났다면 h(48,2) = (48+2^2) mod 7로 3의 위치에 저장되었을 것이다.


하지만 Quadratic probing에도 secondary clustering이라는 단점이 있다. 이는 처음 시작 해쉬값이 같을 경우, 그 이후의 해쉬값들도 모두 동일한 값으로 계산되어 충돌이 반복적으로 일어나 것을 말한다.





3.1.3 Double hashing


Quadratic probing의 secondary clustering를 해결하기 위해서 사용하는 방법이다. 원리는 간단한데 해쉬 함수를 해쉬 함수 2개로 구성하는 것이다.

해쉬 함수는 다음과 같은 형태를 가진다.


h1(k) = k mod m
h2(k) = k mod m2

h(k,i) = (h1(k) + i*h2(k)) mod m


밑은 dobule hasing의 간단한 예제이다.



dobule hashing을 테스트를 위해 간단하게 코드를 작성했다. 실무에서 쓰는 해쉬처럼 공간이 자동적으로 확장되거나 탐색 삭제를 위한 함수는 구현하지 않았고


그냥 삽입시 충돌이 얼마나 일어나는지를 체크하기 위한 코드이다. 테이블의 크기와 입력값의 크기를 조절해가며 충돌을 테스트 할 수 있다



#include <cstdio>
#include <vector>
#include <algorithm>
#include <ctime>
#include <cstdlib>
using namespace std;

vector<int> v,h;
int n,k,m1,m2;

int myrandom (int i) { return std::rand()%i;}
void randGenerator(vector<int>& v, int n,int k){
    srand(time(NULL));
    for(int i=1;i<=2*n;i++){
        v.push_back(i);
    } 
    random_shuffle(v.begin(),v.end(),myrandom);
}

//더블 해싱을 위한 해싱함수 1 
int hashFunc1(int k){
    return k%m1;
}

//더블 해싱을 위한 해싱함수 2 
int hashFunc2(int k){
    return 1+k%m2;
}

//더블 해싱 함수 
int doubleFunc(int k,int i){
    return (hashFunc1(k)+i*hashFunc2(k))%m1;
}

void hashing(vector<int>& v,vector<int>& h, int (*hashFunc)(int,int)){
    int coll, first = -1,count = 0;
    double rate;     
    for(int i=0; i<k; i++){
        int c = 0;
        while(true){
            int key = hashFunc(v[i],c);
            if(h[key] == -1){
            //  printf("%d가 %d 에 저장!\n",v[i],key);
                h[key] = v[i];
                break;  
            }
            else{
            //  printf("%d번쨰 %d가 %d 에서 충돌! 적재율 : %lf\n",i,v[i],key,(double)i/n*100);
                if(first == -1) {   
                    first = v[i];
                    rate = (double)i/n*100;
                }
                count++;
                c++;
            }
            
        }
            
    }
    
    printf("첫 충돌 값 : %d\n",first); 
    printf("총 충돌 수 : %d\n",count);
    printf("첫 충돌 시 적재율 : %lf%%\n",rate);
}

int main(){
    printf("키 값 범위의 크기 :");
    scanf("%d",&k);
    printf("해시테이블 크기(1~n) : ");
    scanf("%d",&n);
    printf("m1, m2 입력 : ");
    scanf("%d %d",&m1,&m2);
    //해시테이릅 크기 증가
    
    printf("\n\n");
    randGenerator(v,n,k);
    for(int i=0;i<n;i++){
        h.push_back(-1);
    }
//  for(int i = 0; i<n; i++){
//      printf("%d ",v[i]);
//  }
    printf("\n");
    hashing(v,h,doubleFunc);
    printf("\n");
//  for(int i = 0; i<h.size(); i++){
//      printf("%d ",h[i]);
//  }
}


<실행 화면>





이 테스트를 위해 먼저 0부터 n까지 중복되지 않는 섞여있는 배열을 만들어내는 함수를 작성하였다.

이는 0부터 n까지 배열에 숫자를 삽입 후, 이를 셔플로 섞는 방법으로 구현하였다. 

그 코드는 다음과 같다.


//난수 배열 생성기 
int myrandom (int i) { return rand()%i;}
vector<int> randGenerator(int n){
    vector<int> v;
    srand(time(NULL));
    for(int i=0;i<=n;i++){
        v.push_back(i);
    } 
    random_shuffle(v.begin(),v.end(),myrandom);
    return v;
}


입력으로 배열의 크기를 받고, 벡터를 반환한다.



먼저 바로 전 포스트에서 작성한 알고리즘이 정상적으로 작동하는지 확인하겠다.




5개의 정렬이 모두 정상적으로 작동함을 확인 할 수 있었다.


모두 정상적으로 작동하는것을 확인 했으니 이제 모든 정렬의 성능을 테스트 해보도록 하겠다.


이 테스트는 10부터 시작하여 배열 범위를 10배씩 증가시키며 진행하였다.



1. n = 10, n = 100



둘 다 수행 시간이 0으로 측정되었다. 100까지 정도의 범위로는 어떤 정렬을 사용해도 크게 상관 없다는 것을 알 수 있다.



2. n = 10,00



1000개 부터는 속도 차이가 눈에 보이기 시작한다.

선택 정렬, 삽입 정렬, 버블 정렬은 모두 시간 복잡도가 n^2이었다. 그에 따라 셋의 시간이 유사하게 나올줄 알았으나.

삽입 정렬은 삽입 하고자 하는 위치를 찾았을 경우 더이상 비교하지 않으며, 최선의 경우 O(n)에 수행되기 때문에 더 빠른 속도를 보였다.

선택 정렬과 버블 정렬은 둘 다 모든 배열을 비교하는 정렬 알고리즘으로 시간이 유사하게 나옴을 알 수 있다.


그리고 합병 정렬과 퀵 정렬은 둘 다 nlgn에 수행되지만, 앞서 말했다 싶이 퀵 정렬이 조금 더 빠른 성능을 보인다.



3. n = 10,000


각 정렬 별로의 시간은 2번에서 설명한 바와 같다.

그리고 하나 더 알수 있는게, t선택,삽입,버블 정렬의 수행 시간이 이전보다 약 100배정도 증가했다는 사실이다.

 이로 시간 복잡도가 O(n^2)이므로 n이 10배 증가하면 100배한다는 것으로 시간 복잡도의 타당성을 알 수 있다.


또한 합병 정렬의 시간복잡도가 nlgn이다. 1000과 10000는 약 13배 정도 차이 나는데 미세한 값으로 변수가 발생하는 부분이 있기 때문에

정확한 증명을 위해서는 더욱 큰 값을 적용할 필요가 있어보인다.


이 와중에도 퀵 정렬은 시간 측정이 안될 정도로 정말 빠른 성능을 보여주었다. 



3. n = 100,000


선택,삽입,버블 정렬은 앞과 같다.



합병 정렬은 숫자가 커질수록 nlgn의 증가율과 점점 가까워지고 있음을 확인하였다. 

(시간 측정은 어느정도 변수가 있기 때문에 정확한 측정값은 확인하기 힘들었다)

퀵 정렬은 정렬 갯수가 10만이 넘어서야 측정이 되었다.


4. n = 1,000,000



선택,삽입,버블 정렬의 시간이 말도안되게 길어지기 시작했기 때문에 저 셋은 테스트하지 않았다.

사실은 시도는 하였지만 지금까지 측정 시간으로 볼 때 이론상 선택, 버블은 9000초, 삽입은 4000초로 약 2시간 반, 1시간 정도 걸리기 때문에 포기하였다.




5. n = 10,000,000



합병정렬은 유사한 증가율을 보이는데 퀵 정렬은 왜인지 갑자기 30배 가까이 증가했다.

그래도 여전히 합병 정렬보다 빠른 속도를 가지고 있다.




1억개부터는 저 둘로도 마냥 빠르게 측정되지 않기 때문에 여기까지만 테스트하였다. 

지금까지 테스트로 퀵 정렬이 상대적으로 높은 성능을 보임을 알 수 있었고, 합병 정렬은 퀵 정렬에 미치지는 못하지만 그래도 나머지 정렬에 비해 상당한 성능임을 확인 할 수 있었다.


또한 버블 정렬과 선택 정렬, 삽입 정렬은 데이터 10만개 이상부터는 실제로 쓰기엔 어려운 성능을 보였다. 그 이하의 데이터에선 합병, 퀵 정렬보단 느리지만 사용해도 크게 무방할것 같다는 생각이 든다.


밑은 이 테스트를 위해 작성한 코드이다.





#include <cstdio>
#include <algorithm>
#include <vector>
#include <ctime>
using namespace std; 

//난수 생성기 
int myrandom (int i) { return rand()%i;}
vector<int> randGenerator(int n){
    vector<int> v;
    srand(time(NULL));
    for(int i=0;i<=n;i++){
        v.push_back(i);
    } 
    random_shuffle(v.begin(),v.end(),myrandom);
    return v;
}

//선택 정렬 
void selectionSort(vector<int> v){
    for(int i=0;i<v.size()-1;i++){
        for(int j=i+1;j<v.size();j++)
            if(v[i]>=v[j])
                swap(v[i],v[j]);
    }
//  printf("--선택 정렬 결과--\n");
//  for(int i = 0; i<v.size();i++)
//      printf("%d ",v[i]);
//  printf("\n\n");
}

// 삽입 정렬 
void insertionSort(vector<int> v){
    for(int i=1;i<v.size();i++){
        int key = v[i], j = i-1;
            while(j>=0 && key <v[j]){
                swap(v[j], v[j+1]);
                j--;
            }
            v[j+1] = key;
    }
//  printf("--삽입 정렬 결과--\n");
//  for(int i = 0; i<v.size();i++)
//      printf("%d ",v[i]); 
//  printf("\n\n");
}

//버블 정렬 
void bubbleSort(vector<int> v){
    for(int i=0;i<v.size()-1;i++){
        for(int j=1; j<v.size()-i;j++)
            if(v[j-1] > v[j])
                swap(v[j-1],v[j]);
    }
//  printf("--버블 정렬 결과--\n");
//  for(int i = 0; i<v.size();i++)
//      printf("%d ",v[i]);
//  printf("\n\n");
}


//병합 정렬 
void merge(vector<int>& v, int s, int e, int m) {
    vector<int> ret;
    int i = s, j = m + 1, copy = 0;
    
    //결과를 저장할 배열에 하나씩 비교하여 저장한다. 
    while (i <= m && j <= e) {
        if (v[i] < v[j])ret.push_back(v[i++]);
        else if (v[i] > v[j])ret.push_back(v[j++]);
    }

    //남은 값들을 뒤에 채워넣어준다. 
    while (i <= m)  ret.push_back(v[i++]);
    while (j <= e)  ret.push_back(v[j++]);
    
    //원래 배열에 복사해준다. 
    for (int k = s; k <= e; k++) {
        v[k] = ret[copy++];
    }
}

void mergeSort(vector<int>& v, int s, int e){
    if(s<e){
        int m = (s+e)/2;
        /*divide, 분할*/
        mergeSort(v,s,m);//s부터 m까지
        mergeSort(v,m+1,e); //m+1부터 e까지 
        /*conquer, 병합*/
        merge(v,s,e,m);
    }
}



//퀵 정렬 
void qsort(vector<int>& v, int s, int e) {
    int pivot = v[s];
    int bs = s, be = e;
    while (s<e) {   
        while (pivot <= v[e]&&s<e) e--;
        if (s>e) break;
        while (pivot >= v[s]&&s<e) s++;
        if (s>e) break;
        std::swap(v[s], v[e]);
    }
    std::swap(v[bs], v[s]);
    if(bs<s)
        qsort(v,bs, s-1);
    if(be>e)
        qsort(v,s+1, be);

}


int main(){
    clock_t start,end;
    int n;// 숫자 갯수
    printf("랜덤 숫자 범위(1~n) : ");
    scanf("%d",&n);
    vector<int> v = randGenerator(n);
    
//  printf("정렬 전 : ");
//  for(int i=0;i<n;i++)
//      printf("%d ",v[i]);
    printf("\n\n");
    start = clock();    
    selectionSort(v);
    end = clock();
    printf("선택 정렬 수행시간 : %lf\n",(double)(end-start)/CLOCKS_PER_SEC);
    
    start = clock();
    insertionSort(v);
    end = clock();
    printf("삽입 정렬 수행시간 : %lf\n",(double)(end-start)/CLOCKS_PER_SEC);
    
    start = clock();
    bubbleSort(v);
    end = clock();
    printf("버블 정렬 수행시간 : %lf\n",(double)(end-start)/CLOCKS_PER_SEC);
    
    vector<int> v2 = v; 
    
    start = clock();
    mergeSort(v2,0,v.size()-1);  
    end = clock();
//  printf("--병합 정렬 결과--\n");
//  for(int i=0;i<v2.size();i++)
//      printf("%d ",v2[i]);
//  printf("\n\n"); 
    printf("병합 정렬 수행시간 : %lf\n",(double)(end-start)/CLOCKS_PER_SEC);


    start = clock();
    qsort(v,0,v.size()-1); 
    end = clock();
//  printf("--퀵 정렬 결과--\n");
//  for(int i=0;i<v.size();i++)
//      printf("%d ",v[i]);
//  printf("\n"); 
    printf("퀵 정렬 수행시간   : %lf\n",(double)(end-start)/CLOCKS_PER_SEC);
    
}

+ Recent posts