問題
英小文字からなる長さ $N$ の文字列 $S$ が与えられる。$S$ の部分文字列 $S[i..i+2\ell)$($\ell\ge1$)で、前半と後半が等しい $S[i..i+\ell)=S[i+\ell..i+2\ell)$ ものを平方と呼ぶ。
次の 2 つを出力せよ。
- 平方の出現の数(組 $(i,\ell)$ の数)
- 異なる平方の数(文字列として異なるものの数)
入力形式
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$)とこの上限に近い値が出た
- 本問の問題文・制約・入力例は自作
概念図
ヒント(クリックで段階的に開示)
ヒント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…a0.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 の原典確認、ハッシュ衝突の実測、区間の質問