Day 058-Q1 — Suffix Array + Lyndon Factorization

2026-06-11 赤色 Master / Phase 8+ ★★★★★★★★★ SA / Duval Algorithm / Booth's Algorithm / 辞書順最小回転

問題

長さ $N$ の文字列 $S$ が与えられる。以下の $Q$ クエリを処理せよ。

  • クエリ 1 l r: $S[l..r]$ を Lyndon 分解し、辞書順最大の Lyndon 語の長さと個数を出力せよ。
  • クエリ 2 l r: $S[l..r]$ の辞書順最小回転の開始位置(0-indexed)を出力せよ。

制約

パラメータ範囲
$N$$1 \le N \le 2 \times 10^5$
$Q$$1 \le Q \le 10^5$
$S$小文字英字のみ
クエリ範囲$1 \le l \le r \le N$(1-indexed)

入出力例

入力例 1

13 2
aababacabacab
1 1 7
2 1 7

出力例 1

2 1
3

$S[1..7]="aababac"$: Lyndon 分解を適用し最大 Lyndon 語を求める。
最小回転はBooth's algorithmで O(N) に解ける。

概念図: Lyndon 分解と最小回転

Duval Algorithm — 3ポインタで Lyndon 分解 文字列: "abcabcd" a b c a b c d Lyndon: "abcabcd" はそれ自体が Lyndon 語(辞書順最小回転 = 自身) Booth's Algorithm — 最小回転の探索 s2 = s + s = "abcabcdabcabcd" KMP failure function を応用してポインタ k を更新 → 最終的に k が最小回転の開始位置 3ポインタ: i(未処理先頭), j(候補先頭比較位置), k(拡張) Duval: s[j] < s[k] → j = i (リセット) | s[j] == s[k] → j++, k++ | s[j] > s[k] → Lyndon語出力 各 Lyndon 語: 長さ k-j の連続コピーを i から出力し i += k-j

ヒント(段階的開示)

ヒント1: 方向性
Lyndon 分解と最小回転は、どちらも Suffix Array または Duval アルゴリズムと密接に関連している。 まずは部分文字列に対して O(N) で Lyndon 分解できる Duval アルゴリズムを実装せよ。 最小回転は Booth's algorithm(KMP の failure function の応用)で O(N)。
ヒント2: アプローチ
  • Lyndon 語の定義: 自身のすべての真の回転よりも辞書順で小さい文字列
  • Duval アルゴリズム: O(N) で文字列を Lyndon 語の積に分解
  • 3ポインタ i, j, k を使い、s[j] <= s[k] の条件で管理
  • Booth's algorithm: s+s に KMP 的手法を適用し O(N) で最小回転位置
ヒント3: コード骨格
def lyndon_factorization(s):
    n, i = len(s), 0
    result = []
    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(s[i:i + k - j])
            i += k - j
    return result

def min_rotation(s):
    s2 = s + s
    n = len(s)
    f = [-1] * (2 * n)
    k = 0
    for j in range(1, 2 * n):
        sj = s2[j]
        i = f[j - k - 1]
        while i != -1 and sj != s2[k + i + 1]:
            if sj < s2[k + i + 1]:
                k = j - i - 1
            i = f[i]
        if sj != s2[k + i + 1]:
            if sj < s2[k]:
                k = j
            f[j - k] = -1
        else:
            f[j - k] = i + 1
    return k

模範解答 (Python)

import sys
input = sys.stdin.readline

def lyndon_factorization(s):
    n, i = len(s), 0
    result = []
    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(s[i:i + k - j])
            i += k - j
    return result

def min_rotation(s):
    s2 = s + s
    n = len(s)
    f = [-1] * (2 * n)
    k = 0
    for j in range(1, 2 * n):
        sj = s2[j]
        i = f[j - k - 1]
        while i != -1 and sj != s2[k + i + 1]:
            if sj < s2[k + i + 1]:
                k = j - i - 1
            i = f[i]
        if sj != s2[k + i + 1]:
            if sj < s2[k]:
                k = j
            f[j - k] = -1
        else:
            f[j - k] = i + 1
    return k

def main():
    N, Q = map(int, input().split())
    S = input().strip()
    for _ in range(Q):
        query = input().split()
        if query[0] == '1':
            l, r = int(query[1]) - 1, int(query[2])
            sub = S[l:r]
            words = lyndon_factorization(sub)
            if not words:
                print(0, 0)
                continue
            max_w = max(words)
            cnt = words.count(max_w)
            print(len(max_w), cnt)
        else:
            l, r = int(query[1]) - 1, int(query[2])
            sub = S[l:r]
            print(min_rotation(sub))

main()

Step-by-Step 解説

Step 1: Lyndon 語の定義

Lyndon 語とは、自身のすべての真の回転よりも辞書順で小さい文字列。例: "abc""bca", "cab" より小さい → Lyndon 語。

Step 2: Duval アルゴリズムの仕組み

3つのポインタ i(未処理先頭), j(現在 Lyndon 語候補の先頭比較位置), k(拡張位置)を使う。

  • s[j] < s[k]: 候補をリセット(j = i)
  • s[j] == s[k]: j++, k++
  • s[j] > s[k]: 長さ k-j の Lyndon 語を i から出力

Step 3: Booth's Algorithm の仕組み

KMP の failure function を応用。s+s に対して処理し、最小回転の開始位置を O(N) で求める。

Step 4: クエリ処理

各クエリで部分文字列を切り出し、対応関数を呼び出す。全体計算量は $O(N + Q \cdot L)$($L$ = クエリ長)。

よくあるミス

ミス原因正しい書き方
s[j] <= s[k] の条件漏れ等号を忘れると Lyndon 分解が壊れるwhile k < n and s[j] <= s[k]
Booth's の初期化f 配列サイズを n にするf = [-1] * (2 * n)
1-indexed/0-indexed 混在クエリ変換ミスl = int(query[1]) - 1

次のステップ

発展問題: 全文字列の Lyndon 分解の結果をオンラインで更新しながら、各 Lyndon 語の出現回数を管理せよ(動的 Lyndon 分解)。

自己評価

解いた後に記入してください。