Day 047-Q2 — ローリングハッシュ+二分探索 最長回文クエリ

2026-05-31 赤色 Master / Phase 8+ ★★★★★★★★★ Rolling Hash / Binary Search / Manacher

問題

長さ $N$ の文字列 $S$(英小文字)が与えられる。$Q$ 個のクエリに答えよ:

クエリ l r:$S[l..r]$(1-indexed)の部分文字列の中で、長さが最大の回文部分文字列の長さを求めよ。

制約

$1 \le N \le 3 \times 10^5$
$1 \le Q \le 10^5$
$1 \le l \le r \le N$
時間制限: 3秒

入出力例

入力例 1

10 3
abacabadab
1 10
3 8
5 7

出力例 1

7
5
3

概念図: ローリングハッシュ二分探索での回文判定

文字列 S: a b a c a b a d a b 中心 c=5 半径 R=2 → "acaba" 長さ5 二分探索で最長回文半径を発見 lo=0, hi=min(c, n-1-c) mid = (lo+hi+1)//2 → is_pal(c-mid, c+mid) でハッシュ比較 True → lo = mid (半径を増やせる) False → hi = mid-1 (半径を減らす) O(log N) 回の is_pal 呼び出し = O(1) each → O(log N) per center

ヒント(段階的開示)

ヒント1: 方向性
各クエリ $[l,r]$ で最長回文部分列を求めたい。Eertree(回文オートマトン)は任意区間クエリには不向き。ローリングハッシュ + 二分探索で中心ごとに最長回文半径を $O(\log N)$ で求め、クエリ範囲内に収まる最大のものを返す戦略を考えましょう。
ヒント2: アプローチ
  • 前処理: 各位置 $c$(奇数長・偶数長)について、最長回文半径 $R[c]$ をローリングハッシュ二分探索で $O(\log N)$ ずつ計算 → 全体 $O(N \log N)$
  • 二重ハッシュ($2^{61}-1$ と $2^{31}-1$ の Mersenne 素数)で衝突を防ぐ
  • クエリ $[l,r]$: 各中心 $c$ について有効半径 $\min(R[c], c-l, r-c)$ を計算 → 最大値を返す
ヒント3: is_pal の実装
def is_pal(l, r):
    # 前向きハッシュ [l..r] と 逆向きハッシュ [n-1-r..n-1-l] を比較
    h1 = (hf1[r+1] - hf1[l] * pf1[r-l+1]) % MOD1
    r1 = (sr1[n-r] - sr1[n-r + (r-l+1)] * pr1[r-l+1]) % MOD1  # 調整
    if h1 != r1: return False
    h2 = (hf2[r+1] - hf2[l] * pf2[r-l+1]) % MOD2
    r2 = (sr2[n-r] - sr2[n-r + (r-l+1)] * pr2[r-l+1]) % MOD2
    return h2 == r2

模範解答 (Python)

import sys
input = sys.stdin.readline

def solve():
    N, Q = map(int, input().split())
    S = input().strip()
    n = len(S)

    MOD1, BASE1 = (1 << 61) - 1, 131
    MOD2, BASE2 = (1 << 31) - 1, 137

    def build_hash(s, base, mod):
        h = [0] * (len(s) + 1)
        pw = [1] * (len(s) + 1)
        for i, c in enumerate(s):
            h[i+1] = (h[i] * base + ord(c)) % mod
            pw[i+1] = pw[i] * base % mod
        return h, pw

    hf1, pf1 = build_hash(S, BASE1, MOD1)
    hf2, pf2 = build_hash(S, BASE2, MOD2)
    sr1, pr1 = build_hash(S[::-1], BASE1, MOD1)
    sr2, pr2 = build_hash(S[::-1], BASE2, MOD2)

    def get_fwd(l, r, h, pw, mod):
        return (h[r+1] - h[l] * pw[r-l+1]) % mod

    def get_rev(l, r, h, pw, mod):
        rl, rr = n-1-r, n-1-l
        return (h[rr+1] - h[rl] * pw[rr-rl+1]) % mod

    def is_pal(l, r):
        if l > r or l == r: return True
        if get_fwd(l, r, hf1, pf1, MOD1) != get_rev(l, r, sr1, pr1, MOD1): return False
        return get_fwd(l, r, hf2, pf2, MOD2) == get_rev(l, r, sr2, pr2, MOD2)

    R_odd = [0] * n
    for c in range(n):
        lo, hi = 0, min(c, n-1-c)
        while lo < hi:
            mid = (lo + hi + 1) >> 1
            if is_pal(c-mid, c+mid): lo = mid
            else: hi = mid - 1
        R_odd[c] = lo

    R_even = [0] * n
    for c in range(n-1):
        lo, hi = 0, min(c+1, n-1-c)
        while lo < hi:
            mid = (lo + hi + 1) >> 1
            if is_pal(c-mid+1, c+mid): lo = mid
            else: hi = mid - 1
        R_even[c] = lo

    out = []
    for _ in range(Q):
        l, r = map(int, input().split())
        l -= 1; r -= 1
        best = 1
        for c in range(l, r+1):
            rad = min(R_odd[c], c - l, r - c)
            best = max(best, 2 * rad + 1)
        for c in range(l, r):
            rad = min(R_even[c], c - l + 1, r - c)
            if rad > 0:
                best = max(best, 2 * rad)
        out.append(best)

    print('\n'.join(map(str, out)))

solve()

Step-by-Step 解説

1ローリングハッシュの二重化
単一ハッシュは衝突の恐れがある。$2^{61}-1$(Mersenne 素数)と $2^{31}-1$ の二重ハッシュでほぼ確実に衝突回避できる。
2回文判定 is_pal(l, r)
前向きハッシュと逆向きハッシュを比較する。$S[l..r]$ と $S_{\text{rev}}[n-1-r..n-1-l]$ のハッシュが等しければ回文。
3各中心の最長回文半径の二分探索
Manacher 法($O(N)$)が最適だが、ローリングハッシュ + 二分探索で $O(N \log N)$ も実用的。各中心 $c$ に対して is_pal(c-mid, c+mid) を二分探索する。
4奇数長・偶数長の別管理
奇数長回文: 中心が位置 $c$、半径 $R$、$S[c-R..c+R]$ が回文。偶数長回文: 中心が $c$ と $c+1$ の間、半径 $R$、$S[c-R+1..c+R]$ が回文。
5クエリへの回答
区間 $[l,r]$ 内の各中心 $c$ について、実際に使える回文半径は $\min(R[c], c-l, r-c)$。本解の学習用実装は $O(N)$ per query。本番は Sparse Table 応用で $O(\sqrt{N})$ や $O(\log^2 N)$ を目指す。

計算量

前処理: $O(N \log N)$(ハッシュ構築 + 全中心の二分探索)
クエリ(本実装): $O(N)$ per query
クエリ(最適化): $O(\sqrt{N})$ per query(平方分割)
空間: $O(N)$

よくあるミス

ミス原因正しい書き方
逆向きハッシュのインデックス計算ミス反転後の位置を誤るrl = n-1-r, rr = n-1-l
偶数長回文の境界条件ミス中心が位置の「間」にあるS[c-rad+1..c+rad] を明示
二分探索の境界 lo < hi vs lo <= hi最大値を取り損なう答えが lo になるよう hi = mid - 1
ハッシュのモジュラー演算で負の値Python の % は正だが計算ミス% mod で常に正にする

次のステップ

  • 発展問題: 区間内の相異なる回文部分文字列の個数カウント(Eertree + Mo's Algorithm)
  • 関連: Day036 Q5(ローリングハッシュ二分探索 最長共通部分文字列)の復習
  • 応用: DNA 配列解析での回文認識(制限酵素切断部位の特定)

自己評価