본문으로 건너뛰기
김신건의 로그

Suffix Array (접미사 배열)

· 수정 · 📖 약 3분 · 847자/단어 #algorithm #string #suffix-array #lcp
suffix array, 접미사 배열, SA, LCP array

정의

Suffix Array (SA, 접미사 배열) 는 문자열 s 의 모든 접미사를 사전순 정렬한 인덱스 배열. SA[i] = 시작 위치 (0-indexed). 보통 LCP (Longest Common Prefix) 배열 을 함께 구축.

s = "banana"
suffixes:
  0: banana      정렬: 5:a, 3:ana, 1:anana, 0:banana, 4:na, 2:nana
SA = [5, 3, 1, 0, 4, 2]
LCP = [0, 1, 3, 0, 0, 2]  // lcp between adjacent suffixes

문제 상황과 동기

문자열의 부분문자열 검색, 반복 패턴, 공통 접두사 등의 문제가 매일 등장.

  • naive: 부분문자열 검색 O(NM). 모든 접미사 정렬 O(N² log N).
  • Suffix Array: O(N log N) 구축 후, 검색 O(M log N) (binary search on SA). LCP 배열로 서로 다른 부분문자열 개수, k 번째 부분문자열, 가장 긴 반복 부분문자열을 O(N) 에 계산.

핵심 통찰: 접미사들을 정렬하면, 문자열의 모든 부분문자열이 접두사로 드러난다.

시각화

핵심 아이디어

Doubling (Manber-Myers): 각 단계에서 접미사를 (rank[i], rank[i+k]) 쌍으로 정렬, k 를 2 배씩 증가. O(N log N).

LCP (Kasai): k = 0; for(i) { if(rank[i]==0) continue; j=SA[rank[i]-1]; while(s[i+k]==s[j+k]) k++; LCP[rank[i]]=k; if(k) k--; }. O(N).

알고리즘

build_sa(s):
    n = len(s)
    초기 rank = s[i] 의 ASCII 값
    k = 1
    while k < n:
        각 접미사의 키 = (rank[i], rank[i + k] or -1)
        std::sort 로 정렬
        새 rank 부여 (같으면 같은 rank)
        k *= 2
    return SA

build_lcp(s, sa):
    rank[sa[i]] = i
    k = 0
    for i in 0..n-1:
        if rank[i] == 0: continue
        j = sa[rank[i] - 1]
        while s[i+k] == s[j+k]: k++
        lcp[rank[i]] = k
        if k > 0: k--
    return LCP

구현

// Suffix Array (doubling) + LCP (Kasai)
#include <bits/stdc++.h>
using namespace std;

vector<int> build_sa(const string& s) {
  int n = s.size();
  vector<int> sa(n), rank(n), tmp(n);
  for (int i = 0; i < n; i++) { sa[i] = i; rank[i] = s[i]; }
  for (int k = 1; k < n; k *= 2) {
      auto cmp = [&](int a, int b) {
          if (rank[a] != rank[b]) return rank[a] < rank[b];
          return (a + k < n ? rank[a + k] : -1) < (b + k < n ? rank[b + k] : -1);
      };
      sort(sa.begin(), sa.end(), cmp);
      tmp[sa[0]] = 0;
      for (int i = 1; i < n; i++) tmp[sa[i]] = tmp[sa[i-1]] + cmp(sa[i-1], sa[i]);
      rank = tmp;
  }
  return sa;
}

vector<int> build_lcp(const string& s, const vector<int>& sa) {
  int n = s.size();
  vector<int> rank(n), lcp(n);
  for (int i = 0; i < n; i++) rank[sa[i]] = i;
  int k = 0;
  for (int i = 0; i < n; i++) {
      if (rank[i] == 0) continue;
      int j = sa[rank[i] - 1];
      while (i + k < n && j + k < n && s[i+k] == s[j+k]) k++;
      lcp[rank[i]] = k;
      if (k) k--;
  }
  return lcp;
}

int main() {
  string s; cin >> s;
  auto sa = build_sa(s);
  auto lcp = build_lcp(s, sa);
  for (int i : sa) cout << i << " ";
  cout << "
";
  for (int i : lcp) cout << i << " ";
}
stdin
banana
결과
5 3 1 0 4 2
0 1 3 0 0 2

복잡도

항목
SA 구축 (doubling)O(N log N) 시간, O(N) 공간
SA 구축 (SA-IS)O(N) 시간, O(N) 공간
LCP 구축 (Kasai)O(N) 시간, O(N) 공간
부분문자열 검색O(M log N) (binary search on SA)
서로 다른 부분문자열 수O(N) (n*(n+1)/2 - Σ LCP)

활용

응용방법복잡도
서로 다른 부분문자열 수n(n+1)/2 - sum(LCP)O(N)
k 번째 부분문자열LCP 누적, k 추적O(N)
최장 반복 부분문자열LCP argmaxO(N)
두 문자열 LCSS+$+T+# 의 SA+LCPO(S+T)
최소 회전s+s 의 SA 에서 첫 N 이내O(N log N)

함정

1. 0-indexed vs 1-indexed

SA 원소는 시작 위치. LCP[0] 은 정의가 없음 (보통 0). 일관성 유지.

2. 정렬 비교 함수

Doubling 에서 (rank[i], rank[i+k]) 쌍이 잘못 들어가면 순서가 꼬임. i+k 가 범위 밖일 때 -1 처리.

3. LCP 와 SA 순서

LCP[i] = s[SA[i-1]] 과 s[SA[i]] 의 공통 접두사 길이 (i >= 1). i=0 은 0.

4. 메모리

SA + LCP + rank = 3× N ints. N = 5×10^5 까지는 OK, 10^6 이상이면 메모리 주의.

BOJ 연습 문제

번호제목정답률링크
BOJ 9248Suffix Array-kokoa-lab
BOJ 11479서로 다른 부분 문자열의 개수 2-kokoa-lab
BOJ 11656접미사 배열-kokoa-lab
BOJ 13264접미사 배열 2-kokoa-lab
BOJ 1605가장 긴 반복 부분문자열-kokoa-lab
BOJ 189793주차 포스터-kokoa-lab

참고

이 글의 용어 (4개)
누적 합 (Prefix Sum)algorithm
정의 누적 합 (Prefix Sum) 은 배열 에 대해 (또는 1-indexed ) 을 미리 계산해 두고, 임의 구간 합 을 O(1) 에 구하는 정형. 문제 풀이에서 "구간 N …
KMP 문자열 매칭 (Knuth-Morris-Pratt)algorithm
정의 KMP (Knuth-Morris-Pratt) 는 Donald Knuth, James H. Morris, Vaughan Pratt 가 1977년 고안한 선형 시간 문자열 매칭…
Suffix Automaton, Suffix Treealgorithm
정의 Suffix Automaton (SAM) 은 문자열 의 모든 부분문자열을 단어로 받는 최소 결정성 유한 오토마타 (minimal DFA). 노드 수 ≤ 2n, 간선 수 ≤ …
Z 알고리즘 (Z Algorithm)algorithm
정의 Z 알고리즘 (Z Algorithm) 은 문자열 의 각 위치 i 에 대해, 와 의 최장 공통 접두사 (LCP) 길이 를 O(N) 시간에 모두 계산하는 선형 알고리즘. 결과 …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기