Day 135-Q4 — Suffix Automatonによる相異なる部分文字列の長さ総和

2026-08-27 赤色 Master / Phase 8+ ★★★★★★★★☆ 各状態のlen区間の等差数列和を合算 O(N)

問題

文字列$S$(長さ$N$)が与えられる。$S$に含まれる相異なる部分文字列すべてについて、その長さの総和を求めよ。

例えば$S=$"aab"のとき、相異なる部分文字列はa, aa, aab, ab, bの5個で、長さの総和は$1+2+3+2+1=9$である。

入力形式

S

制約

$1 \le |S| \le 5\times10^5$
$S$は英小文字のみ

入出力例

入力例1

aab

出力例1

9

入力例2

aaaa

出力例2

10

概念図

状態vが表す長さ区間 (len[link[v]], len[v]] 0 len[link[v]]=1 len[v]=4 この区間の整数(2,3,4)がそれぞれ1つの相異なる部分文字列の長さ 寄与 = Σ(2..4) = (2+4+1)(4-2)/2 = 9 全状態(初期状態を除く)についてこの和を合算 → O(N)

ヒント(段階的開示)

ヒント1(方向性)

SAMの各状態(初期状態を除く)は「長さの区間$(\text{len}[\text{link}[v]],\text{len}[v]]$」に対応する部分文字列の集合を表す。今回は個数ではなく「長さの総和」を求める点が違う。

ヒント2(アプローチ)

状態$v$が表す部分文字列の長さは$\text{len}[\text{link}[v]]+1,\dots,\text{len}[v]$のちょうど1つずつに対応する。その寄与は等差数列の和$\sum_{k=\text{len}[\text{link}[v]]+1}^{\text{len}[v]}k$になる。

ヒント3(誘導)

等差数列の和の公式$\sum_{k=a+1}^{b}k=\frac{(a+b+1)(b-a)}{2}$($a=\text{len}[\text{link}[v]]$, $b=\text{len}[v]$)を使う。SAMの構築は標準的なオンライン構築で$O(N)$。

def add_char(c):
    global last
    cur = new_state(len=states[last]['len'] + 1)
    p = last
    while p != -1 and c not in states[p]['trans']:
        states[p]['trans'][c] = cur
        p = states[p]['link']
    ...

模範解答 (Python)

import sys


def build_sam(s):
    MAXN = 2 * len(s) + 5
    length = [0] * MAXN
    link = [-1] * MAXN
    trans = [dict() for _ in range(MAXN)]

    size = 1
    last = 0
    length[0] = 0
    link[0] = -1

    for ch in s:
        cur = size
        size += 1
        length[cur] = length[last] + 1
        p = last
        while p != -1 and ch not in trans[p]:
            trans[p][ch] = cur
            p = link[p]
        if p == -1:
            link[cur] = 0
        else:
            q = trans[p][ch]
            if length[p] + 1 == length[q]:
                link[cur] = q
            else:
                clone = size
                size += 1
                length[clone] = length[p] + 1
                trans[clone] = dict(trans[q])
                link[clone] = link[q]
                while p != -1 and trans[p].get(ch) == q:
                    trans[p][ch] = clone
                    p = link[p]
                link[q] = clone
                link[cur] = clone
        last = cur

    return size, length, link


def main():
    s = sys.stdin.readline().strip()
    size, length, link = build_sam(s)

    total = 0
    for v in range(1, size):
        a = length[link[v]]
        b = length[v]
        total += (a + b + 1) * (b - a) // 2

    print(total)


main()

Step-by-Step 解説

1SAMの状態が表す部分文字列集合
各状態$v$は同じendpos集合を持つ部分文字列たちの集まりを表し、その長さは$\text{len}[\text{link}[v]]+1$から$\text{len}[v]$までの連続区間を1回ずつカバーする。
2個数から総和へ
標準の「個数」問題では$\text{len}[v]-\text{len}[\text{link}[v]]$を足すが、今回は区間に含まれる整数の総和を等差数列公式で$O(1)$計算し全状態を走査する。全体$O(N)$。
3SAM構築のロジック
新状態curを作り、lastからlinkを辿りながら未定義の遷移にcurへのリンクを張る。既存の遷移に当たったらlenの一致でリンク先を決めるかクローンするか分岐する。
4クローン処理の意味
1状態=1連続長さ区間という不変量を保つため、区間を分割する必要がある場合にクローンを作る。
5計算量
状態数は高々$2N-1$個。$N=5\times10^5$では辞書使用の定数倍に注意が必要。

よくあるミス

ミス原因正しい書き方
個数の公式をそのまま総和に使う「個数」と「長さの総和」を混同等差数列の和$(a+b+1)(b-a)/2$を使う
初期状態も加算してしまうループ範囲をrange(size)にする初期状態は寄与しないのでrange(1,size)にする
クローンのtransを浅いコピーでなく参照で共有trans[clone]=trans[q]で同じ辞書を共有trans[clone]=dict(trans[q])で必ずコピーする
linkの更新順序ミスlink[q]を先に上書きしてから元の値を使う代入と値の使用順序を混同しないよう注意する
境界処理漏れN=1のケースをテストしない短い文字列でも状態数が正しいか確認する

次のステップ

  • 発展: 出現回数の総和(延べ出現数×長さ)を求める場合、endposサイズをparent tree上のDPで求めてから同様に集計せよ。
  • 発展2: 複数文字列の共通部分文字列に限定した一般化Suffix Automatonの構築を検討せよ。
  • 次回予告: 次回のテーマは今後のセッションで選定

自己評価

自分の回答

気づき・メモ