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 SAbuild_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 << " ";}
def build_sa(s): n = len(s) sa = list(range(n)) rank = [ord(c) for c in s] tmp = [0] * n k = 1 while k < n: sa.sort(key=lambda x: (rank[x], rank[x + k] if x + k < n else -1)) tmp[sa[0]] = 0 for i in range(1, n): prev, cur = sa[i-1], sa[i] pk = rank[prev + k] if prev + k < n else -1 ck = rank[cur + k] if cur + k < n else -1 tmp[cur] = tmp[prev] + (rank[prev] != rank[cur] or pk != ck) rank = tmp k <<= 1 return sadef build_lcp(s, sa): n = len(s) rank = [0] * n for i, pos in enumerate(sa): rank[pos] = i lcp, k = [0] * n, 0 for i in range(n): if rank[i] == 0: continue j = sa[rank[i] - 1] while i + k < n and j + k < n and s[i+k] == s[j+k]: k += 1 lcp[rank[i]] = k if k: k -= 1 return lcps = input().strip()sa = build_sa(s)lcp = build_lcp(s, sa)print(*sa)print(*lcp)
import java.util.*;import java.io.*;public class Main { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s = br.readLine(); int n = s.length(); Integer[] sa = new Integer[n]; int[] rank = new int[n], tmp = new int[n]; for (int i = 0; i < n; i++) { sa[i] = i; rank[i] = s.charAt(i); } for (int k = 1; k < n; k *= 2) { final int K = k; final int[] R = rank; Arrays.sort(sa, (a, b) -> { if (R[a] != R[b]) return Integer.compare(R[a], R[b]); return Integer.compare(a+K<n?R[a+K]:-1, b+K<n?R[b+K]:-1); }); tmp[sa[0]] = 0; for (int i = 1; i < n; i++) { int a = sa[i-1], b = sa[i]; boolean diff = R[a] != R[b] || (a+K<n?R[a+K]:-1) != (b+K<n?R[b+K]:-1); tmp[b] = tmp[a] + (diff ? 1 : 0); } rank = tmp; } StringBuilder sb = new StringBuilder(); for (int x : sa) sb.append(x).append(" "); System.out.println(sb); }}
stdin
banana
결과
5 3 1 0 4 20 1 3 0 0 2
stdin
ababa
결과
4 2 0 3 10 1 3 0 1
복잡도
항목
값
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 argmax
O(N)
두 문자열 LCS
S+$+T+# 의 SA+LCP
O(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 이상이면 메모리 주의.
💬 댓글