Day 177-Q5 — 2026-10-13 | runs から平方を数える(出現の総数は run ごとの閉じた式、異なる平方の数は run ごとに p 個以下の候補)

2026-10-13 赤色 Master (Phase 8+) ★★★★★★★★★ 文字列(焦点帯 M4)。Day175-Q5 で runs(極大反復)を全部列挙した。今回はその使い道で、平方($ww$ の形の部分文字列)を数える。平方の出現は $\Theta(N^2)$ 個(aaaa…)ありうるので 1 つずつは数えられないが、どの平方の出現もちょうど 1 つの run に属するので、run ごとの閉じた式で足せる。異なる平方の数は、run の中では開始位置が周期 $p$ ずれると同じ文字列になることを使い、候補を run ごとに絞る

問題

英小文字からなる長さ $N$ の文字列 $S$ が与えられる。$S$ の部分文字列 $S[i..i+2\ell)$($\ell\ge1$)で、前半と後半が等しい $S[i..i+\ell)=S[i+\ell..i+2\ell)$ ものを平方と呼ぶ。

次の 2 つを出力せよ。

  1. 平方の出現の数(組 $(i,\ell)$ の数)
  2. 異なる平方の数(文字列として異なるものの数)

入力形式

S

出力形式

1 行目に出現の数、2 行目に異なる平方の数

制約

#制約
1$1\le N\le10^5$

入出力例

入力例1

mississippi

出力例1

5
4

(ss が 2 回、pp、ississ、ssissi。ss が重なるので異なるのは 4 個)

入力例2

aaaa

出力例2

4
2

(aa が 3 回と aaaa が 1 回。aaaa は根 aa が原始的でない平方で、周期 1 の run aaaa の中で $\ell=2p$ として数える)

入力例3

abaababaab

出力例3

6
5

(aa が 2 回、abab、baba、abaaba、そして文字列全体 abaab+abaab。3 例とも、全 $(i,\ell)$ を調べる全探索と一致)

背景と出典

  • runs(極大反復)と runs 定理(run の数は $N$ 未満。Bannai–I–Inenaga–Nakashima–Takeda–Tsuruta, SIAM J. Comput. 2017 と記憶、本文は未確認)は Day175-Q5 で実装した。本問はその実装をそのまま使う
  • 「どの平方の出現もちょうど 1 つの run に含まれる」は Crochemore らの文献でよく使われる事実と記憶している(原典は本セッションで未確認)。本問では Fine–Wilf の定理から自分で示し、全探索で確かめた
  • 異なる平方の数は高々 $N$ 個という結果(Fraenkel–Simpson 1998 と記憶、未確認)があり、Fibonacci 語 $N=10^5$ で 71341 個($0.71N$)とこの上限に近い値が出た
  • 本問の問題文・制約・入力例は自作

概念図

S = caabaabaabc。周期 p = 3 の run S[1..10)(長さ L = 9)に含まれる長さ 6 の平方 c 0 a 1 a 2 b 3 a 4 a 5 b 6 a 7 a 8 b 9 c 10 aab | aab 候補(異なる平方) aba | aba 候補(異なる平方) baa | baa 候補(異なる平方) aab | aab p ずれ → 1 つ目と同じ 出現の数: K = ⌊9/6⌋ = 1 で L − 2p + 1 = 4 個。異なるもの: min(p, 4) = 3 個(aabaab, abaaba, baabaa) この他に周期 1 の run「aa」が 3 つあり、平方 aa の出現が 3 個・異なる平方は 1 個(run をまたぐ重複はハッシュで除く)

ヒント(クリックで段階的に開示)

ヒント1(方向性)

平方 $ww$($|w|=\ell$)は周期 $\ell$ を持つ長さ $2\ell$ の文字列。その最小周期 $p$ と $\ell$ の関係を考える。$p$ を保ったまま左右に延ばすと何になるか。

ヒント2(アプローチ)
  • Fine–Wilf の定理: 長さ $\ge p+\ell-\gcd(p,\ell)$ の文字列が周期 $p$ と $\ell$ を持てば、周期 $\gcd(p,\ell)$ も持つ。長さ $2\ell\ge p+\ell$ なので $\gcd(p,\ell)$ も周期で、最小性から $p\mid\ell$
  • 平方を周期 $p$ のまま延ばしきると、周期 $p$ の run がちょうど 1 つ決まる。逆に run $(l,r,p)$(長さ $L=r-l$)の中では、$\ell=kp$ の平方が $L-2kp+1$ 個ある
ヒント3(誘導)
  • 出現の数 $=\sum_{\text{run}}\sum_{k=1}^{K}(L-2kp+1)$、$K=\lfloor L/2p\rfloor$。閉じた式は $K(L+1)-pK(K+1)$
  • 異なる平方: run の中で開始位置が $p$ ずれると同じ文字列。だから run ごと・$k$ ごとに先頭 $\min(p,\ L-2kp+1)$ 個だけが候補。run をまたぐ重複(同じ根の共役を持つ別の run)はハッシュで除く

模範解答 (Python)

sa_is・LCE・lyndon_ends は Day175-Q5 の模範解答と同じなので省略する(そのまま貼り付けて使う)。

模範解答を表示する(まず自力で解いてから)
import sys

# ---- ここに Day175-Q5 の sa_is, class LCE, lyndon_ends をそのまま置く ----


def compute_runs(a):
    # Day175-Q5 と同じ: 2 順序の Lyndon 配列から候補 2N 個、LCE で左右に延ばす
    n = len(a)
    fw = LCE(a)
    bw = LCE(a[::-1])
    runs = set()
    for inv in (False, True):
        end = lyndon_ends(a, fw, inv)
        for i in range(n):
            j = end[i]
            p = j - i
            r = j + (fw.query(i, j) if j < n and a[i] == a[j] else 0)
            if r - i < 2 * p and (i == 0 or a[i - 1] != a[j - 1]):
                continue
            l = i - (bw.query(n - i, n - j) if i > 0 and a[i - 1] == a[j - 1] else 0)
            if r - l >= 2 * p:
                runs.add((l, r, p))
    return runs


def main():
    import random
    s = sys.stdin.readline().strip()
    n = len(s)
    a = [ord(c) - 96 for c in s]
    runs = compute_runs(a)

    # (1) 平方の出現数: 平方 ww の最小周期 p は |w| を割り切り、周期 p の run にちょうど 1 つ含まれる
    occ = 0
    for l, r, p in runs:
        L = r - l
        K = L // (2 * p)                       # |w| = kp(k = 1..K)
        occ += K * (L + 1) - p * K * (K + 1)   # Σ_k (L - 2kp + 1)

    # (2) 異なる平方の数: run の中では開始位置が p ずれると同じ文字列 → 先頭 min(p, 出現数) 個が候補。
    #     run をまたいだ重複はロリングハッシュ(法 2^61-1)で除く
    MOD = (1 << 61) - 1
    B = random.randrange(10 ** 6, MOD - 1)
    H = [0] * (n + 1)
    PW = [1] * (n + 1)
    for i, c in enumerate(a):
        H[i + 1] = (H[i] * B + c) % MOD
        PW[i + 1] = PW[i] * B % MOD
    seen = set()
    for l, r, p in runs:
        L = r - l
        k2p = 2 * p
        while k2p <= L:
            cnt = L - k2p + 1
            for st in range(l, l + (p if p < cnt else cnt)):
                h = (H[st + k2p] - H[st] * PW[k2p]) % MOD
                seen.add((k2p, h))
            k2p += 2 * p
    print(occ)
    print(len(seen))


main()

計算量: runs の列挙が $O(N\log N)$(Day175-Q5、Sparse Table が律速)。出現の数は run ごとに $O(1)$。異なる平方の候補は $\sum_{\text{run}}\sum_k\min(p,L-2kp+1)\le\sum_{\text{run}}L/2$ 個。$N=10^5$ の 6 種で 0.28〜0.70 s・67〜84 MB(Python 3.14.8、Apple M5 Pro、負荷平均 4〜13 での実測、起動込み)。

Step-by-Step 解説

1平方の最小周期は $\ell$ を割り切る

平方 $ww$(長さ $2\ell$)は周期 $\ell$ を持つ。最小周期を $p\le\ell$ とすると、長さ $2\ell\ge p+\ell>p+\ell-\gcd(p,\ell)$ なので Fine–Wilf の定理から $\gcd(p,\ell)$ も周期。$p$ は最小なので $\gcd(p,\ell)=p$、つまり $p\mid\ell$。

2平方はちょうど 1 つの run に入る

平方の出現 $S[i..i+2\ell)$ は最小周期 $p$、長さ $\ge2p$。これを周期 $p$ のまま左右に延ばしきった区間は、最小周期 $p$ の run。延ばし方は一意なので、属する run も一意。逆に run $(l,r,p)$ の中の長さ $2kp$ の部分文字列はすべて平方(周期 $p$ を持つので前半と後半が等しい)で、最小周期は $p$(run の最小周期が $p$ で、長さ $\ge2p$ の部分の最小周期も $p$)。

3出現の数

run の長さ $L=r-l$、$K=\lfloor L/2p\rfloor$ として

$$\sum_{k=1}^{K}(L-2kp+1)=K(L+1)-pK(K+1)$$

aaaa…($N=10^5$)では run が 1 つで答えは $2.5\times10^9$。1 つずつ数えたら終わらない量を $O(1)$ で足す。

4異なる平方の候補

run の中で長さ $2kp$ の平方の開始位置 $st$ と $st+p$ は同じ文字列(周期 $p$)。開始位置 $l..l+p-1$ は根の $p$ 個の共役で、根が原始的なので互いに異なる。よって run・$k$ ごとに $\min(p,L-2kp+1)$ 個が「その run の中で異なる」候補。

5run をまたぐ重複

根の共役が同じ別の run(例: abaab… の離れた 2 か所)は同じ平方を作る。これを除くために(長さ, ハッシュ)の集合に入れる。ハッシュは法 $2^{61}-1$・乱数の基数で、衝突確率は候補 $10^5$ 個程度なら $10^{-8}$ 程度と見積もる(見積もりで、測ってはいない)。重複を除かずに足すと、845 個のテスト中 509 個で数が多すぎた(実測)。

迷った他案と採らなかった理由

案計算量採否理由
全 $(i,\ell)$ を文字列比較$O(N^3)$検算用のみ$N\le200$ 程度
全 $(i,\ell)$ を LCE で判定$O(N^2)$不採用(遅い)$N=3000$ で 0.36 s、$6000$ で 1.47 s(本解 0.011・0.022 s)。$N=10^5$ では 400 s 程度と見込む
run ごとに $k=1$(原始的な根の平方)だけ数える$O(\text{runs})$不採用(誤り)aaaa の aaaa を落とす。845 個中 653 個で出現の数を誤る(実測)
異なる平方を $k=1$ だけで数える同上不採用(誤り)845 個中 653 個で誤る(実測)
run をまたぐ重複を除かない同上不採用(誤り)845 個中 509 個で多く数える(実測)
異なる平方を接尾辞配列のランクで厳密に区別$O(\text{候補}\log N)$不採用衝突の心配は無くなるが、同じ長さの候補をランクと LCE で比べる手間が要る。本問ではハッシュの衝突確率を見積もって受け入れた
runs + 閉じた式 + ハッシュ(本解)$O(N\log N)$採用0.28〜0.70 s

前提が変わったら

  • 確率的な誤りが許されない(ハッシュ不可)なら、候補の開始位置を接尾辞配列のランクで並べ、同じ長さで LCE が長さ以上のものをまとめる
  • 平方でなく $k$ 乗($w^k$)を数えるなら、同じ run の分解で $\ell=jp$ の $k$ 乗が $L-kjp+1$ 個、と式を変えるだけで済む
  • 区間ごとの質問($S[l..r]$ の中の平方の数)なら、run を区間で切った長さで同じ式を使い、run の数 $O(N)$ に対する区間の集計(BIT など)が要る

よくあるミス

ミス原因正しい書き方
run ごとに $L-2p+1$ だけ足す原始的な根の平方だけを見る$\ell=kp$ の全 $k$ を足す(入力例 2)
出現の数を 32 ビット整数で持つ最大 $N^2/4\approx2.5\times10^9$Python の整数(他言語なら 64 ビット)
異なる平方の候補を run の全開始位置にする周期 $p$ のずれで同じになることを忘れる先頭 $\min(p,\cdot)$ 個で十分(候補数が $L$ から $p$ に減る)
run をまたぐ重複を除かない1 つの run の中だけで考える(長さ, ハッシュ)の集合で除く
ハッシュの基数を固定衝突を狙った入力に弱い実行ごとに乱数で選ぶ

次のステップ

  • 発展1: 異なる平方をハッシュなしで(接尾辞配列のランクで)数え、時間を本解と比べる
  • 発展2: 3 乗(cube)の出現と異なる数を同じ枠組みで数える
  • 発展3: Fibonacci 語で異なる平方の数 / $N$ が何に近づくかを $N=10^3\sim10^6$ で測る

検証ログ(本セッションで実行した数値)

  • 全探索との照合(実測): 長さ 1〜60・文字種 1〜3 の乱数 500 本(別プロセスで実行)、さらに乱数 600 本(長さ 80 以下)・Fibonacci 語/Thue–Morse 語/a…a/abab…/aabaab… の接頭辞(長さ 1〜197 を 7 刻み)・$u^iv u^j$ 型 100 本の計 845 本で、出現の数・異なる平方の数とも全件一致
  • 誤った方法(実測、845 本): $k=1$ だけで出現 653、$k=1$ だけで異なる数 653、重複を除かない 509 で誤答
  • 時間(実測、起動込み、$N=10^5$): 乱数 3 文字 0.48 s(出現 50030・異なる 506)、乱数 2 文字 0.51 s、a…a 0.28 s(2500000000・50000)、Fibonacci 語 0.70 s(1044482・71341)、Thue–Morse 語 0.53 s(83311・56)、abaab の繰り返し 0.45 s。メモリ 67〜84 MB
  • $O(N^2)$ 版との比較(実測、乱数 3 文字): $N=3000$ で 0.36 s 対 0.011 s、$N=6000$ で 1.47 s 対 0.022 s(答えは一致)
  • 扱っていない範囲: runs 定理・Fraenkel–Simpson の原典確認、ハッシュ衝突の実測、区間の質問

自己評価

自分の回答

気づき・メモ