問題
英小文字からなる文字列 $S$ が与えられる。$S$ の空でない相異なる部分文字列の個数を求めよ。
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $|S|$ | $1 \le |S| \le 2 \times 10^5$ | 文字列長 |
| 文字種 | 英小文字のみ | $|\Sigma| = 26$ |
入出力例
入力例1
abab
出力例1
7
部分文字列は a, b, ab, ba, aba, bab, abab の 7 種類。
概念図: SAM の状態と長さ区間の寄与
ヒント
ヒント1(方向性)
相異なる部分文字列数は Suffix Automaton (SAM) で $O(N)$。各状態は endpos が等しい部分文字列の同値類で、担う文字列長は区間 $(\text{len}[\text{link}], \text{len}]$ を成す。
ヒント2(アプローチ)
初期状態を除く各状態 $v$ について $\text{len}[v] - \text{len}[\text{link}[v]]$ の総和が答え。各状態が「その状態でのみ表現される部分文字列の個数」を担う。
ヒント3(ほぼ答え)
# online 構築 + clone 処理(len[p]+1 != len[q] のとき)
ans = sum(length[v] - length[link[v]] for v in range(1, sz))
模範解答
import sys
def solve():
S = sys.stdin.readline().strip()
N = len(S)
MAX = 2 * N + 5
nxt = [dict() for _ in range(MAX)]
link = [0] * MAX
length = [0] * MAX
link[0] = -1
last = 0
sz = 1
for ch in S:
cur = sz; sz += 1
length[cur] = length[last] + 1
p = last
while p != -1 and ch not in nxt[p]:
nxt[p][ch] = cur
p = link[p]
if p == -1:
link[cur] = 0
else:
q = nxt[p][ch]
if length[p] + 1 == length[q]:
link[cur] = q
else:
clone = sz; sz += 1
length[clone] = length[p] + 1
nxt[clone] = dict(nxt[q])
link[clone] = link[q]
while p != -1 and nxt[p].get(ch) == q:
nxt[p][ch] = clone
p = link[p]
link[q] = clone
link[cur] = clone
last = cur
ans = 0
for v in range(1, sz):
ans += length[v] - length[link[v]]
print(ans)
solve()
計算量: 状態数・遷移数ともに $O(N)$、構築 $O(N \cdot |\Sigma|)$(dict 使用で実質 $O(N)$)。
Step-by-Step 解説
Step 1: SAM の状態と endpos
各状態は endpos(出現終端位置集合)が等しい文字列の同値類。同値類内の長さは連続区間 $(\text{len}[\text{link}[v]], \text{len}[v]]$。
Step 2: online 構築
文字を 1 つずつ追加し、新状態 `cur` を作って `last` から suffix link を辿り遷移を張る。
Step 3: clone 処理
| 条件 | 処理 |
|---|---|
| $\text{len}[p]+1 = \text{len}[q]$ | $\text{link}[\text{cur}] = q$ |
| $\text{len}[p]+1 \ne \text{len}[q]$ | $q$ を分裂させ clone を作り link を張り替え |
Step 4: 部分文字列数の集計
各状態 $v$ は $\text{len}[v] - \text{len}[\text{link}[v]]$ 個の新規部分文字列を担う。総和が答え。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| clone 遷移の参照共有 | nxt[clone]=nxt[q] | dict(nxt[q]) で複製 |
| 配列サイズ不足 | 状態は最大 $2N-1$ | MAX = 2*N+5 |
| 初期状態を集計に含める | link[0]=-1 | range(1, sz) から集計 |
次のステップ
- 発展問題: 各部分文字列の出現回数総和(endpos サイズ × 長さ、suffix link 木上の累積)
- 発展問題: 辞書順 $k$ 番目の相異なる部分文字列(SAM 上の DAG DP + 貪欲)
自己評価
理解度: / /
自分の回答:
気づき・メモ: