Z 알고리즘 (Z Algorithm)
정의
Z 알고리즘 (Z Algorithm) 은 문자열 s[0..N-1] 의 각 위치 i 에 대해, s[i..] 와 s[0..] 의 최장 공통 접두사 (LCP) 길이 를 O(N) 시간에 모두 계산하는 선형 알고리즘.
결과 배열 Z[i] 는:
Z[i] = max { k : s[0..k-1] == s[i..i+k-1] }
Z[0] 은 정의되지 않거나 N (전체 길이)
KMP 와 비슷한 역할 (패턴 매칭 O(N+M)) 이지만, 구현이 더 직관적이고 짧아서 대회에서 선호됨.
문제 상황과 동기
길이 M 패턴 P 를 길이 N 텍스트 S 에서 찾는다.
- naive: 모든 위치 i 에서 P 와 S[i..] 를 비교. O(N·M).
- KMP: O(N+M), 하지만 failure function 구현이 복잡.
- Z 알고리즘:
s = P + '$' + S(delimiter) 로 합친 후 Z 배열 계산.Z[i] == M인 i 가 매칭 위치. O(N+M), 코드 ~20줄.
핵심 통찰: 이미 매칭된 구간 (Z-box) 을 최대한 재사용. amortized O(N).
시각화
핵심 아이디어
Z-box: 위치 i 까지 계산할 때, [l, r] 구간은 “가장 오른쪽으로 뻗은 매칭 구간” (s[l..r] == s[0..r-l]).
invariant: r = max { i + Z[i] - 1 } (지금까지)
새 i 계산:
- i > r: naive 비교 시작, Z[i] 갱신, (l, r) 갱신.
- i ≤ r:
s[i..r]는s[i-l..r-l]와 일치 (Z-box 내부).Z[i-l]을 재사용:Z[i-l] < r - i + 1:Z[i] = Z[i-l](재사용 가능)Z[i-l] ≥ r - i + 1: r 너머 확장 시도 필요, naive 비교로 연장
amortized O(N): r 은 최대 N-1 까지 증가, 한 번 증가하면 다시 감소 안 함. 총 비교 횟수 O(N).
알고리즘
z_function(s):
N = len(s)
Z = [0] * N
l, r = 0, 0
for i = 1 to N-1:
if i > r:
k = 0
while i + k < N and s[k] == s[i + k]:
k++
Z[i] = k
if k > 0:
l, r = i, i + k - 1
else:
k = i - l
if Z[k] < r - i + 1:
Z[i] = Z[k]
else:
k = r - i + 1
while i + k < N and s[k] == s[i + k]:
k++
Z[i] = k
l, r = i, i + k - 1
return Z
구현
// Z algorithm: O(N) LCP of s[i..] and s[0..]
#include <bits/stdc++.h>
using namespace std;
vector<int> z_function(const string& s) {
int n = s.size();
vector<int> z(n);
int l = 0, r = 0;
for (int i = 1; i < n; i++) {
if (i > r) {
l = r = i;
while (r < n && s[r - l] == s[r]) r++;
z[i] = r - l; r--;
} else {
int k = i - l;
if (z[k] < r - i + 1) z[i] = z[k];
else {
l = i;
while (r < n && s[r - l] == s[r]) r++;
z[i] = r - l; r--;
}
}
}
return z;
}
int main() {
string s; cin >> s;
auto z = z_function(s);
for (int i = 1; i < (int)s.size(); i++)
cout << z[i] << (i + 1 == s.size() ? "\n" : " ");
}aaabaab2 1 0 2 1 0복잡도
| 항목 | 값 |
|---|---|
| 시간 | O(N) (amortized, r 이 한 방향으로만 증가) |
| 공간 | O(N) (Z 배열) |
| 비교 횟수 | ≤ 2N (각 위치 최대 한 번 방문) |
패턴 매칭 응용
길이 M 패턴 P, 길이 N 텍스트 S:
string s = P + "$" + S; // '$' 는 P, S 어디에도 없는 delimiter
auto z = z_function(s);
for (int i = M + 1; i < (int)s.size(); i++) {
if (z[i] == M) {
// i - M - 1 이 S 에서의 매칭 시작 위치
}
}
O(N+M) 시간, O(N+M) 공간.
변형 / 활용
1. 최장 접두사 = 접미사
s + s 의 Z 배열에서 Z[i] + i == 2N 인 i 찾기. O(N).
2. 회문 판정
s + reverse(s) 의 Z 배열. 하지만 매내처 가 더 빠름.
3. 주기 찾기
Z[i] ≥ N - i 인 가장 작은 i. 문자열의 최소 주기 길이 i.
4. 다중 패턴 매칭
P1 + ’$’ + P2 + ’#’ + S 같은 방식으로 여러 패턴 동시 처리. 하지만 Aho-Corasick 이 더 효율적.
함정
1. Z[0] 정의
Z[0] 은 미정의 또는 N. 코드에 따라 0 또는 N. 보통 쓰지 않음.
2. delimiter 누락
패턴 매칭 시 P + S 로 합치면 P 끝과 S 시작이 이어져서 오판. 반드시 P + '$' + S 로 구분.
3. r < l 상태
r 이 i-1 보다 작을 때 i > r 조건 실패하면 l, r 갱신 안 되어 버그. 구현 정확히.
4. 중복 비교
while (r < n && s[r-l] == s[r]) r++ 루프가 amortized O(N) 임을 신뢰하지 못하고 불필요한 최적화 시도. 그대로 둬야 함.
KMP vs Z 알고리즘
| 비교 | KMP | Z |
|---|---|---|
| 복잡도 | O(N+M) | O(N+M) |
| 코드 길이 | ~40줄 (failure func + search) | ~20줄 |
| 직관성 | failure function 이해 어려움 | Z-box 재사용 명확 |
| 응용 | 오토마타 기반 확장 가능 | prefix 관련 문제 직접 해결 |
| 대회 선호 | 낮음 | 높음 (간결) |
대부분의 경우 Z 가 더 간단하므로 KMP 대신 Z 사용 권장.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 16916 | 부분 문자열 | 53.1% | kokoa-lab |
| BOJ 11585 | 속타는 저녁 메뉴 | 45.2% | kokoa-lab |
| BOJ 1701 | Cubeditor | 38.9% | kokoa-lab |
| BOJ 13576 | Prefix와 Suffix | 32.1% | kokoa-lab |
참고
- KMP 알고리즘 (failure function 기반 패턴 매칭)
- 문자열 해싱 (확률적 O(1) 비교)
- 매내처 알고리즘 (최장 회문 O(N))
- Aho-Corasick (다중 패턴 매칭 오토마타)
이 글의 용어 (4개)
- 매내처 알고리즘 (Manacher's Algorithm)algorithm
- 정의 매내처 알고리즘 (Manacher's Algorithm) 은 문자열 의 모든 위치에서 중심으로 하는 최장 회문 (palindrome) 반지름 을 O(N) 시간에 계산하는 선…
- 문자열 해싱 (String Hashing)algorithm
- 정의 문자열 해싱 (String Hashing) 은 문자열을 고정 길이의 정수 (해시값) 로 매핑하여, 부분 문자열 비교를 O(1) 에 수행할 수 있게 만드는 기법. Rabin과…
- Aho-Corasick (아호-코라식)algorithm
- 정의 Aho-Corasick 은 Alfred Aho 와 Margaret Corasick 이 1975년 고안한 다중 패턴 문자열 매칭 알고리즘. Trie 에 KMP failure …
- KMP 문자열 매칭 (Knuth-Morris-Pratt)algorithm
- 정의 KMP (Knuth-Morris-Pratt) 는 Donald Knuth, James H. Morris, Vaughan Pratt 가 1977년 고안한 선형 시간 문자열 매칭…
💬 댓글