더 이상 tistory 블로그를 운영하지 않습니다. glanceyes.github.io에서 새롭게 시작합니다.

새소식

알고리즘/개념 정리

접미사 배열(Suffix Array)과 LCP(Longest Common Prefix)

  • -

 

개인적으로 알고리즘은 즉각적으로 바로 구현할 수 있도록 몸에 베어야 하되 복습하는 데 너무나 많은 시간을 투자해서는 안 되고, 문제를 풀면서 실전으로 익혀야 한다고 생각한다. 학기가 시작되면서 바빠진 만큼 지난 잊힌 알고리즘 개념들을 핵심과 코드만 짧게 정리하여 평소에도 자주 보면서 익숙해지고자 한다. 기본적으로 코드는 특별한 설명이 없으면 C++를 기반으로 한다.

 

 

 

접미사 배열(Suffix)

 

정의

 

문자열 S의 모든 접미사들을 사전 순으로 정렬한 배열.

여기서 접미사들은 문자열 S에서 시작 위치 번호로 관리한다.

 

 

문제

 

문자열 S="abcbca"의 접미사 배열을 구해보자.

 

접미사: "abcbca", "bcbca", "cbca", "bca", "ca", "a"

사전 순으로 정렬 시 "a", "abcbca", "bca", "bcbca", "ca", "cbca"

 

문자열 길이가 N일 때, 일일이 접미사 문자열을 비교하면서 사전 순으로 정렬하려면?

집미사 사전 순 정렬 시: O(NlogN)

각 접미사마다 문자열 일일이 비교: O(N)

 

O(NlogN)×O(N)=O(N2logN) → 시간복잡도를 줄일 수는 없을까?

 

핵심은 문자열 S의 접미사의 접두사는 S의 부분 문자열임을 이용한다.

 

 

 

O(NlogN2) 방법

 

 

LCP(Longest Common Prefix)

 

사전 순으로 정렬한 접미사 배열에서 인접한 접미사끼리 최대로 겹치는 접두사의 길이

 

 

예시

 

S = "abcbca"의 접미사 배열을 사전 순으로 정렬하기

그룹 번호로 정렬된 인접한 두 접미사의 접두사를 길이 1, 2, 4, 8, 처럼 2i 만큼씩 비교하며 정렬해 가는데, 새롭게 정렬한 순서가 새로운 그룹 번호가 된다.

 

빈 문자열 " "을 0으로 시작하여, abcbca", "bcbca", "cbca", "bca", "ca", "a"에 각각 접미사 번호가 1부터 6까지 붙어 있다고 가정한다.

즉, 빈 문자열을 제외한 접미사의 시작 위치가 문자열에서 몇 번째 문자부터 시작하는지 그 위치대로 접미사 번호가 붙는다고 가정한다.

 

 

(1)

  " " abcbca a bcbca bca cbca ca
접미사 번호 6 0 5 1 3 2 4
그룹 번호 -1 0 0 1 1 2 2

 

길이 1만큼의 접두사끼리만 비교해서 사전 순으로 그룹 번호를 부여한다.

동일한 접두사를 지니면 같은 그룹 번호를 부여한다.

처음에는 길이 1만큼의 접두사를 비교하므로 결국 접미사의 맨 앞 문자의 사전순으로 그룹 번호를 부여한다고 생각해도 되며, 실제 코드로 구현할 때도 접미사의 맨 앞 문자 기준으로 정렬하면 된다.

 

 

(2) 

 

  " " a abcbca bcbca bca ca cbca
접미사 번호 6 5 0 1 3 4 2
그룹 번호 -1 0 1 2 2 3 4

 

이전 그룹 번호로 정렬된 상태에서 인접한 두 접미사의 길이 2만큼의 접두사끼리 비교해서 새로 정렬한 후 그룹 번호를 부여한다.

a와 abcbca의 비교에서 a 뒤의 문자열은 비어 있는데, 빈 문자열에 관한 우선순위는 가장 높으므로 a의 그룹 번호가 더 작게 된다.

 

 

(3)

 

마찬가지로 이전 그룹 번호 기준으로 정렬된 상태에서 인접한 두 접미사의 길이 4만큼의 접두사끼리 비교하여 새로 정렬한 후 그룹 번호를 부여한다.

 

 

이를 일반화하면 다음과 같이 정리할 수 있다.

 

길이 2K만큼의 접두사끼리 비교하는 경우

(1)  두 접미사에서 길이 K만큼의 접두사를 비교한다.

(2) 같은 접두사를 지니면 뒤의 길이 K만큼의 부분 문자열을 비교한다.

 

 

 

 

코드

 

suffix_array[i]: 접미사 배열에서 i번째 위치한 원소가 문자열에서 어느 위치에서 시작하는 접미사인지 그 위치 (접미사 번호)

 

group[i][j]: 접미사 배열을 완성하기 위해 정렬 과정에서 필요한 배열 (길이에 대응되는 level i에 따른 그룹 번호 j)

 

vector<int> suffix_array(n); vector<vector<int>> group(G_MAX, vector<int>(n+1)); int checkCount(int i, int j, int k, int n){ int cnt = 0; while (i < n && j < n && k >= 0){ cnt += (1 << k); i += (1 << k); j += (1 << k); } } // 배열 초기화 for (int i = 0; i < n; i++){ suffix_array[i] = i; group[0][i] = s[i]; } group[0][n] = -1; int last = 0; for (int k = 0, len = 1; len / 2 < n; k++, len *= 2){ auto cmp = [&](int u, int v){ if (group[k][u] == group[k][v]){ return group[k][u + len] < group[k][v + len]; } else { return group[k][u] < group[k][v]; } }; sort(suffix_array.begin(), suffix_array.end(), cmp); last = k + 1; group[last][suffix_array[0]] = 0; for (int i = 1; i < n; i++){ if (cmp(suffix_array[i - 1], suffix_array[i])){ group[last][suffix_array[i]] = group[last][suffix_array[i - 1]] + 1; } else { group[last][suffix_array[i]] = group[last][suffix_array[i - 1]]; } } group[last][n] = -1; }

 

 

 

 

Contents

글 주소를 복사했습니다

부족한 글 끝까지 읽어주셔서 감사합니다.
보충할 내용이 있으면 언제든지 댓글 남겨주세요.