이사 중...


===============================================



하다가 멘탈 깨져서 정리함. 


서픽스 어레이의 n^2logn 풀이는 아주 간단하다. 소스 코드만 정리해보자면 ... 



1
2
3
4
5
6
7
8
9
10
vector<int> getSuffixArray(const string& s){
    vector<int> suffix; 
    //suffix배열을 초기화
    for (int i = 0; i < s.size(); ++i) suffix.push_back(i); 
    //문자열을 비교( 람다함수 사용 )
    sort(suffix.begin(), suffix.end(), [&s](int a, int b){
        return strcmp(s.c_str() + a, s.c_str() + b) < 0
    });
    return suffix; // 리턴
}
cs

 


아아아아아주 쉽다. 오히려 람다함수가 더 어려운듯...


방식자체는 설명하자면 n개의 문자열을 죄다 때려박은 후

(메모리를 적게 사용하기 위해서 인덱스만 때려박음 : 4번째 줄)


그 해당 문자열을 정렬한다. 근데 정렬하는데 드는 비용이 nlogn이고 하나의 문자열을 비교하는 비용이 n이므로 n^2logn이다. 

굉장히 못써먹을 시간복잡도라고 생각할 수 있지만 사실 문자열을 비교하는데 일반적인 경우, 항상 n이 걸리지는 않기 때문에 그럭저럭 쓸만하다.

... 인풋이 aaaaaaaaaaaaaaaaaaaa... 만 아니라면... 


이것보다 좋은 알고리즘으로는 맨버-마이어스의 알고리즘이 있다. 

이 알고리즘의 시간복잡도는 n * log n * log n  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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
std::vector<int> getSuffixArray(const string& s) {
    int n = s.size();
    
    //idx + t 까지 비교한다!
    int t = 1
 
    //group[i]는 t까지 비교했을 때 i의 그룹 번호 
    vector<int> group(n + 1);
    //t==0일때를 대신한 거라고 봐도 무방... 
    for (int i = 0; i < n; ++i) group[i] = s[i];
    group[n] = -1;
 
    //서픽스를 담을 배열
    vector<int> suffix(n);
    //서픽스를 초기화
    for (int i = 0; i < n; ++i) suffix[i] = i;
 
 
    while (t < n) {
 
        //비교함수 아주 중요함
        auto cmp = [&group, t](int a, int b){
            if (group[a] != group[b])
                return group[a] < group[b];
            return group[a + t] < group[b + t];
        };
 
        //정렬
        std::sort(suffix.begin(), suffix.end(), cmp);
 
        t *= 2;
 
        //t가 n보다 크거나 같으면 종료
        if (t >= n) break;
 
        //그룹 재지정
        vector<int> newGroup(n + 1);
        newGroup[n] = -1;
        newGroup[suffix[0]] = 0;
        for (int i = 1; i < n; ++i){
            if (cmp(suffix[i - 1], suffix[i])){
                newGroup[suffix[i]] = newGroup[suffix[i - 1]] + 1;
             }else
                newGroup[suffix[i]] = newGroup[suffix[i - 1]];
        }
        group = newGroup;
    }
    return suffix;
}
cs

 



람다함수 [&group,t] <= group은 레퍼런스로 참조하고 t는 값을 복사해서 사용한다. 

나름대로 정리하자면 

s ? 구하려는 서픽스 배열의 target 스트링

group[i] ? i번째 접미사가 가지는 그룹 번호(index순) 

suffix[i] ? t까지 봤을때 i번째 접미사(사전 순)


t상태일때 어디까지 정렬되냐면 글자수가 2t일때까지 정렬된다.(즉 1일때는 글자수가 2개까지 정렬)

근데 이 그룹이라는 놈이 엄청나게 많은 것을 함축 하고 있는데 

group[a]는 사실 string의 [a...a+t)까지 비교한 결과의 상대 값이 저장되어 있다고 보면 된다.

이걸 이해하는데 이틀걸림. 


22~ 26줄의 compare함수를 보자. 


case : group[a] != group[b]

group[a]와 group[b]가 다를 때 group[a] < group[b]를 리턴한다. 

현재 t상태일때 벌써부터 group값이 다르다는 것은 이미 비교가 끝나있다는 것이다. 

그러니 a+t번째는 비교해볼 가치도 없음. 


case : group[a]와 group[b]가 같다.

string의 [a...a+t) [b...b+t)의 비교결과가 같다는 것이다. 

그럼이제 이걸 신경 쓸 필요가 없지.

[a+t...a+2t)와 [b+t...b+2t) 만 비교하면 된다. 

근데 이걸 일일이 다 비교하면 시간복잡도가 어마어마하게 증가한다. 

비교 할때마다 t만큼 추가 시간이 걸리니 말이다.

그리고 다행스럽게도 이 결과는 이미 저장되어 있다. 


어디에?

group[a+t]와 group[b+t]에!


그렇기 때문에 group[a+t]와 group[b+t]를 비교한 값을 리턴해주면 된다.


이 비교함수로 정렬이 완료되었으면 이제 이 결과를 다시 그룹에 반영해줘야 하는데 

이부분이 37~46줄이다. 

그룹의 값은 상대값이기 때문에 suffix의 첫번째 값은 0으로 설정하도록 하자. 

이제부터 suffix[n-1]까지 갱신해주는데 비교했는데 

이 비교의 기준은 이전 group과 t이다.(아직 갱신되기 전)

(실수로 t를 값이아니라 레퍼런스로 참조해봤는데 재밌는 일이 일어났다 ㅎㅎ)


case : suffix[i-1]과 suffix[i]를 비교

비교값이 더크데! <- 당연히 더 커야지 비교한 결과가... 

즉 이미 비교가 완료된 항목임.

그러니까 상대값도 1증가시켜줌. 

case : 현재까지 문자열을 사전순으로 비교할 수 없다. 

suffix배열은 t까지 진행했을때 정렬되어 있어야한다. 

즉 이부분은 아직 정렬이 제대로 이뤄지지 않았다는 것이고

따라서 아직까지는 suffix[i]와 suffix[i-1]은 누가더 사전순으로 우선인지 알 수 없다.

즉 suffix[i]와 suffix[i-1]은 길이가 2t일때까지 같았다는 것 

그룹번호를 같게 유지해서 다음 시퀀스에 비교할 수 있게 해두자.

(서픽스 어레이의 특성상 모든 문자열의 길이가 다르기때문에 같은건 존재할수없다.)


다른사람이 이해가 됐을지 모르겠다. 


내가 나중에 봤을때 이해할 수 있을 정도로만 정리해둔거라... 


틀린부분 있다면 지적부탁드려용.


참고 : 알고리즘 문제해결 전략 2권 suffix array파트 참조


p.s. suffix array관한 풀이는 최대 n까지 줄일 수 있고 이것보다 쉬운 nlogn풀이도 존재합니다.

p.s.2 그니까 저건 나중에 공부해서 포스팅할게요. 이거하는데도 지침 + sa와 한쌍인 lcp알고리즘도 다음에... 저가 너무 멍청해서


======================================================


이전 블로그에 있던 글 옮겨왔습니다. ㅎ 

'Problem Solving > 이론' 카테고리의 다른 글

nCr mod p 구하기  (2) 2018.04.26
거듭 제곱의 합  (2) 2016.10.22

+ Recent posts