問題
文字列 S(英小文字)が与えられる。以下のQ個のクエリに答えよ。
A i j: S[i..j] の異なる回文部分文字列の数B i j: S[i..j] の最長回文部分文字列の長さC i j: S[i..j] の各回文部分文字列の出現回数の総和
制約
$|S| \le 2 \times 10^5$
$Q \le 10^5$
$0 \le l \le r < |S|$
入出力例
入力例 1
aabaa
5
A 0 4
B 0 4
C 0 4
A 0 2
B 1 3
出力例 1
4
5
8
3
3
ヒント (段階的開示)
ヒント1: 方向性
Eertree(回文オートマトン)は文字列の全回文部分文字列を O(N) で管理するデータ構造。各ノードが一つの異なる回文部分文字列に対応。
ヒント2: アプローチ
2つの根(長さ-1の奇数根と長さ0の偶数根)を持ち、各ノードはlen, link(suffix link), edgesを持つ。左から走査しながら木を拡張。
ヒント3: 誘導
def get_link(v, i):
while i - lens[v] - 1 < 0 or s[i - lens[v] - 1] != s[i]:
v = links[v]
return v
模範解答 (Python)
import sys
from collections import defaultdict
input = sys.stdin.readline
class Eertree:
def __init__(self):
self.lens = [-1, 0]
self.links = [0, 0]
self.edges = [defaultdict(int), defaultdict(int)]
self.cnt = [0, 0]
self.last = 1
self.s = []
self.node_count = 2
def _get_link(self, v, i):
while i - self.lens[v] - 1 < 0 or self.s[i - self.lens[v] - 1] != self.s[i]:
v = self.links[v]
return v
def add(self, c):
self.s.append(c)
i = len(self.s) - 1
cur = self._get_link(self.last, i)
if c not in self.edges[cur]:
new_len = self.lens[cur] + 2
if new_len == 1:
new_link = 1
else:
prev = self._get_link(self.links[cur], i)
new_link = self.edges[prev].get(c, 1)
self.lens.append(new_len)
self.links.append(new_link)
self.edges.append(defaultdict(int))
self.cnt.append(0)
self.edges[cur][c] = self.node_count
self.node_count += 1
self.last = self.edges[cur][c]
self.cnt[self.last] += 1
def propagate(self):
for i in range(self.node_count-1, 1, -1):
self.cnt[self.links[i]] += self.cnt[i]
def main():
S = input().strip()
Q = int(input())
for _ in range(Q):
line = input().split()
t, l, r = line[0], int(line[1]), int(line[2])
sub = S[l:r+1]
et2 = Eertree()
for c in sub:
et2.add(c)
et2.propagate()
if t == 'A':
print(et2.node_count - 2)
elif t == 'B':
max_len = max(et2.lens[i] for i in range(2, et2.node_count)) if et2.node_count > 2 else 0
print(max_len)
else:
total = sum(et2.cnt[i] for i in range(2, et2.node_count))
print(total)
main()
Step-by-Step 解説
1Eertreeの構造
nodes[0]: 長さ-1の根(奇数長回文の親)、nodes[1]: 長さ0の根(偶数長回文の親)。各ノードに回文部分文字列とsuffix link。
nodes[0]: 長さ-1の根(奇数長回文の親)、nodes[1]: 長さ0の根(偶数長回文の親)。各ノードに回文部分文字列とsuffix link。
2文字の追加
各文字追加時、現在位置 i での最長回文接尾辞を suffix link を辿って探す。見つかった回文の左に今の文字を追加した回文が新しい接尾辞。
各文字追加時、現在位置 i での最長回文接尾辞を suffix link を辿って探す。見つかった回文の左に今の文字を追加した回文が新しい接尾辞。
3カウントの伝播
suffix link の逆順に cnt を伝播することで各回文ノードの総出現回数が計算可能。
suffix link の逆順に cnt を伝播することで各回文ノードの総出現回数が計算可能。
4クエリへの応用
異なる回文部分文字列数 = ノード数 - 2、出現回数総和 = propagate後のcnt合計。
異なる回文部分文字列数 = ノード数 - 2、出現回数総和 = propagate後のcnt合計。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 根の初期化ミス | odd.link=0が必須 | odd.link = 0(自己参照で無限ループ防止) |
| get_link の境界 | i - len - 1 < 0 のチェック忘れ | while条件の最初にチェック |
| cnt伝播の順序 | suffix linkは必ず短い方へ | node_count-1から降順に伝播 |
次のステップ
- 発展: Eertreeを使った「回文の回文」カウント
- 応用: Online Palindrome Queries