suffix array
suffix array(접미사 배열)은 다양한 문자열 문제를 푸는데 사용될 수 있다. suffix array는 말 그대로 어떤 문자열 S에서 나올 수 있는 모든 접미사를 사전 순으로 정리해 놓은 것이며, 정렬되어 있기 때문에 이분탐색을 통한 문자열 검색이 가능하다.
ex) S = “algorithm”
| array_index | start_index | S[start_idndex…] | 
|---|---|---|
| 0 | 0 | algorithm | 
| 1 | 2 | gorithm | 
| 2 | 7 | hm | 
| 3 | 5 | ithm | 
| 4 | 1 | lgorithm | 
| 5 | 8 | m | 
| 6 | 3 | orithm | 
| 7 | 4 | rithm | 
| 8 | 6 | thm | 
맨버-마이어스 알고리즘
접미사배열을 구할 때, 접미사를 모두 배열에 넣고 각 언어의 내장 sort 함수를 사용하여 구할 수도 있지만, O(n) 안으로 동작하는 맨버-마이어스 알고리즘을 알아본다.
vector<int> getSuffixArray(const string & s){
    int n = s.size();
    int t = 1;
    // group 배열
    // 각 접미사들을 [0:t]를 기준으로 정렬했을 때 속하는 그룹을 나타냄
    vector<int> group(n+1);
    // 초기 세팅에서는 그냥 문자열 character를 그대로 사용한다.
    // 0부터 시작하는 값은 아니지만 대소 관계 비교에는 문제가 없다.
    for(int i = 0 ; i < n ; ++i) group[i] = s[i];
    // group은 길이가 0인 접미사도 포함하며 -1로 세팅한다.
    group[n] = -1;
    // return array
    vector<int> ret(n);
    for(int i = 0 ; i < n ; ++i) ret[i] = i;
    
    while(t < n){
        // compare 함수
        auto compare = [&group, t](int a, int b){
            // 첫 t글자가 다르면 판단 가능
            if(group[a] != group[b]) return group[a] < group[b];
            // 아니면 s[a+t:]와 s[b+t:]를 비교한다.
            // group[a] == group[b]이므로 두 길이는 t 이상임을 알 수 있다.
            // a + t, b + t 모두 n보다 작으므로 out of index는 없다.
            return group[a+t] < group[b+t];
        };
        sort(ret.begin(), ret.end(), compare);
        t *= 2;
        if(t >= n) break;
        vector<int> newGroup(n+1);
        newGroup[n] = -1;
        newGroup[ret[0]] = 0;
        for(int i = 1 ; i < n ; ++i){
            if(compare(ret[i-1], ret[i])) newGroup[ret[i]] = newGroup[ret[i-1]] + 1;
            else newGroup[ret[i]] = newGroup[ret[i-1]];
        }
        group = newGroup;
    }
    return ret;
}해당 알고리즘은 S[0:1], S[0:2], S[0:4], S[0:8], … 을 기준으로 logn번 정렬을 한다.
Example) S = “banana”
한 글자 기준
| 0 | 1 | 2 | 
|---|---|---|
| anana  ana a  | 
banana | nana  na  | 
두 글자 기준
| 0 | 1 | 2 | 3 | 
|---|---|---|---|
| a | anana  ana  | 
banana | nana  na  | 
네 글자 기준
| 0 | 1 | 2 | 3 | 4 | 5 | 
|---|---|---|---|---|---|
| a | ana | anana | banana | na | nana | 
정렬 과정에서 위의 표 처럼 각 문자열은 특정 t까지의 접미사를 기준으로 특정 group에 속하게 된다. 이 정보를 유지하고 있다면, 두 접두사의 대소비교를 상수 시간에 할 수 있다.
S[a:]와 S[b:]를 비교할 때, group[a] != group[b]라면 이미 결정된 상태이고, group[a] == group[b]라면 group[a + t]와 group[b + t]를 비교하면 된다.
LCP(Longest Common Prefix) array
suffix array와 함께 문제 해결에 사용되는 배열이다. suffix array를 기반으로 구현을 할 수 있으며 [i-1]과 [i]의 가장 긴 접두사 길이를 저장한다.
Example) banana
| i | sa[i] | suffix | lcp[i] | 
|---|---|---|---|
| 0 | 5 | a | x | 
| 1 | 3 | ana | 1 | 
| 2 | 1 | anana | 3 | 
| 3 | 0 | banana | 0 | 
| 4 | 4 | na | 0 | 
| 5 | 2 | nana | 2 | 
vector<int> getLCPArray(const string& s, vector<int> sa){
    int n = s.size();
    vector<int> lcp(n);
    // 접미사 시작 인덱스 : suffix array 인덱스
    vector<int> rank(n);
    for(int i = 0 ; i < n ; i++) rank[sa[i]] = i;
    int matched = 0;
    for(int i = 0 ; i < n ; i++){
        int k = rank[i];
        if (k){
            for(int j = sa[k-1] ; s[i + matched] == s[j + matched] ; matched++);
            lcp[k] = matched;
 
            if (matched) --matched;
        }
    }
    return lcp;
}마지막으로 이를 활용해 해결할 수 있는 문제를 몇 가지 게시한다.
- 9248 : Suffix Array
 - 3033 : 가장 긴 문자열
 - 1605 : 반복 부문문자열
 - 13264 : 접미사 배열2
 - 11479 : 서로 다른 부분 문자열의 개수 2
 - 9249 : 최장 공통 부분 문자열
 
참고
- 구종만, 프로그래밍 대회에서 배우는 알고리즘 문제 해결 전략