問題
長さ $N$ の文字列 $S$(小文字英字のみ)に対して以下の2種類のクエリを $Q$ 回処理せよ。
- クエリ type 1:
1 l r— $S[l..r)$(0-indexed 半開区間)の Lyndon 分解を求め、各 Lyndon 語の長さを空白区切りで出力せよ。 - クエリ type 2:
2 l r— $S[l..r)$ の辞書順最小回転(Booth's Algorithm)の開始位置(相対 0-indexed)を出力せよ。
制約
| パラメータ | 範囲 |
|---|---|
| $N$ | $1 \le N \le 2 \times 10^5$ |
| $Q$ | $1 \le Q \le 10^4$ |
| $l, r$ | $0 \le l < r \le N$ |
| 文字種 | 小文字英字のみ |
| 時間制限 | 2秒 |
入出力例
入力例 1
11 3
abababacaba
1 0 8
2 0 8
1 3 11
出力例 1
2 2 2 2
0
4 4
"ababab ac" (S[0..8]) の Lyndon 分解: "ab"×4 = [2,2,2,2]。最小回転開始位置 = 0("ababacab" が最小回転)。
概念図: Duval Algorithm の動作
ヒント(段階的開示)
ヒント1: 方向性
Lyndon 語とは「それ自身の全ての真の回転の中で辞書順最小」な文字列(= 全ての真の接尾辞より小さい)。
Duval Algorithm は 3 ポインタ
i, j, k で $O(N)$ の Lyndon 分解を実現する。
ヒント2: アプローチ
- Duval Algorithm:
i= 未処理先頭、j= 周期リセット位置、k= 比較位置 s[j] == s[k]: 両方進める(周期の延長候補)s[j] < s[k]:j = iにリセット(より長い Lyndon 語が作れる)s[j] > s[k]:(k-j)の周期で Lyndon 語を切り出す- Booth's Algorithm:
S+S上で KMP failure 関数的処理。最小回転位置を追跡
ヒント3: コード骨格
def lyndon_decomposition(s):
result = []
n = len(s); i = 0
while i < n:
j, k = i, i + 1
while k < n and s[j] <= s[k]:
if s[j] < s[k]: j = i # リセット
else: j += 1
k += 1
while i <= j:
result.append(k - j)
i += k - j
return result
def booth(s):
ss = s + s; n = len(ss)
f = [-1] * n; k = 0
for j in range(1, n):
sj = ss[j]; i = f[j - 1 - k]
while i != -1 and sj != ss[k + i + 1]:
if sj < ss[k + i + 1]: k = j - i - 1
i = f[i]
if sj != ss[k + i + 1]:
if sj < ss[k]: k = j
f[j - k] = -1
else:
f[j - k] = i + 1
return k
模範解答 (Python)
import sys
input = sys.stdin.readline
def lyndon_decomposition(s):
"""Duval Algorithm: O(N) で Lyndon 分解"""
result = []
n = len(s); i = 0
while i < n:
j, k = i, i + 1
while k < n and s[j] <= s[k]:
if s[j] < s[k]:
j = i
else:
j += 1
k += 1
while i <= j:
result.append(k - j)
i += k - j
return result
def booth(s):
"""Booth's Algorithm: O(N) で最小回転位置を返す"""
ss = s + s
n = len(ss)
f = [-1] * n
k = 0
for j in range(1, n):
sj = ss[j]
i = f[j - 1 - k]
while i != -1 and sj != ss[k + i + 1]:
if sj < ss[k + i + 1]:
k = j - i - 1
i = f[i]
if sj != ss[k + i + 1]:
if sj < ss[k]:
k = j
f[j - k] = -1
else:
f[j - k] = i + 1
return k
def solve():
N, Q = map(int, input().split())
S = input().strip()
out = []
for _ in range(Q):
line = input().split()
t, l, r = int(line[0]), int(line[1]), int(line[2])
sub = S[l:r]
if t == 1:
lengths = lyndon_decomposition(sub)
out.append(' '.join(map(str, lengths)))
else:
pos = booth(sub)
out.append(str(pos))
print('\n'.join(out))
solve()
Step-by-Step 解説
1Lyndon 語の定義
文字列 $w$ が Lyndon 語 ⟺ $w$ の全ての真の接尾辞 $s$ に対して $w < s$(辞書順)。 等価条件: $w$ が自身の全ての真の回転の中で辞書順最小。
文字列 $w$ が Lyndon 語 ⟺ $w$ の全ての真の接尾辞 $s$ に対して $w < s$(辞書順)。 等価条件: $w$ が自身の全ての真の回転の中で辞書順最小。
2Duval Algorithm の核心
ポインタ
ポインタ
i(未処理先頭)、j(周期候補の先頭)、k(比較位置)。
$s[j] < s[k]$ のとき $j = i$ にリセット = 「長い Lyndon 語が作れる可能性」を探る。
$s[j] > s[k]$ のとき、$s[i..k-1]$ は $(k-j)$ を周期に持ち、$s[i..i+(k-j)-1]$ が Lyndon 語。
3Booth's Algorithm
$S+S$ 上で KMP の failure 関数を計算しながら、最小回転の開始位置 $k$ を追跡。 $ss[j] < ss[k+i+1]$ のとき $k = j - i - 1$(より小さい候補に更新)。
$S+S$ 上で KMP の failure 関数を計算しながら、最小回転の開始位置 $k$ を追跡。 $ss[j] < ss[k+i+1]$ のとき $k = j - i - 1$(より小さい候補に更新)。
4Lyndon 分解の一意性
任意の文字列は Lyndon 語の辞書順非増加積 $w_1 \ge w_2 \ge \ldots \ge w_k$ として一意に分解される(Chen-Fox-Lyndon 定理)。
任意の文字列は Lyndon 語の辞書順非増加積 $w_1 \ge w_2 \ge \ldots \ge w_k$ として一意に分解される(Chen-Fox-Lyndon 定理)。
計算量
Duval Algorithm: $O(N)$ 時間・$O(1)$ 追加空間
Booth's Algorithm: $O(N)$ 時間・$O(N)$ 追加空間
クエリ処理: $O(Q \cdot (r-l))$ 全体
最悪: $O(Q \cdot N)$ — 大きな $Q, N$ では事前分解が必要
Booth's Algorithm: $O(N)$ 時間・$O(N)$ 追加空間
クエリ処理: $O(Q \cdot (r-l))$ 全体
最悪: $O(Q \cdot N)$ — 大きな $Q, N$ では事前分解が必要
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
j = i のリセット忘れ | s[j] < s[k] 時に j を i に戻さない | if s[j] < s[k]: j = i |
| 切り出しループの条件 | i <= j でなく i < k にしてしまう | while i <= j: i += k - j |
Booth の f[j-k] 境界 | j - k が負になりうる | k を更新してから f[j-k] を参照 |
| 半開区間の切り出し | S[l:r+1] としてしまう | S[l:r](Python は半開) |
次のステップ
- 発展問題: Lyndon-Schützenberger 定理($u^a = v^b$ ならば共通の Lyndon 根を持つ)の実装検証
- 関連: Suffix Array の隣接要素と Lyndon 分解の関係(SA の連続区間 = Lyndon 語の対応)
- 応用: 辞書順最小回転文字列をキーとした文字列の正規化・比較