1. 程式人生 > 實用技巧 >【刷題記錄】較簡單的 SAM 題選做

【刷題記錄】較簡單的 SAM 題選做

【HDU 4336】Typewriter

你需要生成一個字串 \(S\),一開始你有一個空串,你可以花費 \(p\) 的代價在尾端追加任意字元,或花費 \(q\) 在尾端追加一個當前串的子串。求得到 \(S\) 的最小花費

首先有一個 dp:\(f_i\) 表示得到 \(S\) 的一個長度為 \(i\) 的字首的最小花費。那麼轉移:

\[f_i = \min(f_{i-1} + p, f_j + q) \]

其中 \(j\) 是滿足 \(S[:j]\) 中存在一個子串為 \(S[j+1:i]\) 的最小的 \(j\)。至於為什麼最小,顯然 \(f_{i} < f_{i+1}\)

那麼考慮維護兩個位置 \(i,j\)

(意義如上),對於每個 \(i\),都令 \(j\) 後移直至轉移條件被滿足。然後就是如何實時判斷條件是否被滿足的問題了。

於是整一個 SAM 維護 \(S[:j]\) 部分,並且記錄下 SAM 上當前匹配的位置 \(x\)。對於每個 \(i\),我們先讓 \(x\) 一直往回跳 \(\text{link}\),保證當前狀態可以表示的最短串的長度 \(\text{minlen}(x) \ge i-j\)。跳完之後看看是否存在 \(S_i\) 的轉移,如果有就直接走,否則將 \(S_{j+1}\) 新增到 SAM 中。

【Codeforces 235C】Cyclical Quest

給定一個文字串 \(S\)

\(q\) 次詢問,每次給定一個模式串 \(T\),求 \(T\) 的所有迴圈同構在 \(S\) 中出現次數之和。

首先一個 Naive 的想法就是將 \(T\)\(|T|\) 個迴圈同構都在 \(S\) 的 SAM 上跑一遍,然後加起來,這樣必 T 無疑。又或者是將 \(T\) 複製一遍然後在 SAM 上跑一邊,但這樣會多算所以會 WA。

於是考慮一個簡單的改進:每次刪掉頭字元再新增尾字元。尾部新增字元顯然就是走轉移,而刪去頭字元則對應著跳 \(\text{link}\)

在實現時,我們還是先複雜,只不過要保證每次匹配後面之後要強制條 \(\text{link}\),直至長度範圍剛好包含 \(|T|\)

注意多次詢問不要亂 memset,最好加個時間戳優化。

【SPOJ NSUBSTR】Substrings

給定一個字串 \(S\),對於 \(F(x)\) 為長度為 \(x\)\(S\) 的子串在 \(S\) 中的出現次數的最大值。\(\forall i\in[1, n]\) 求出 \(F(i)\)

首先對 \(S\) 建出 SAM,然後對於每個狀態 \(x\),它的長度範圍為 \([\text{minlen}(x), \text{len}(x)]\),每個長度的子串都出現了 \(|\text{endpos}(x)|\) 次。那麼肯定先預處理 \(\text{enpdos}\) 集的大小,然後 \(\forall i\in [\text{minlen}(x), \text{len}(x)]:F(i)\gets \max(F(i),|\text{endpos}(x)|)\)

看起來向什麼區間最值,於是我一開始直接寫了棵線段樹,但實際上沒必要:對於一個長度為 \(i\) 的子串,如果它出現了 \(k\) 次,那麼必然存在長度為 \(j(<i)\) 的子串它的出現次數 \(\ge k\)

於是就不用區間賦值,直接對 $F(\text{len}(x)) $ 修改即可,最後取一邊字尾 \(\max\) 就做完了。

【HDU4436】str2int

給定 \(n\) 個數字串,對每個數字串提取其所有子串(不含前導 \(0\)),求本質不同的數字子串的對應整數的和。

比較經典的 SAM 上的 dp。首先解決多串的問題,SAM 只針對單串,因此考慮拼接這些數字串。然而直接拼接會多算跨越兩個子串的數字,於是在每兩個數字串間加入分隔符即可,並規定不走這個分隔符的轉移。

建出 SAM 之後設計 dp:\(f_i\) 表示第一次不走 \(0\) 轉移到狀態 \(i\) 的方案數;\(g_i\) 表示所有 \(f_i\) 種方案數對應數字之和。轉移方程:

\[\begin{aligned} f(y) &= \sum_{\delta(x,c)=y} f(x) \\ g(y) &= \sum_{\delta(x,c)=y} 10\cdot g(x) + c \cdot f(x) \end{aligned} \]

考慮到前導 \(0\),最好從根開始往後 dp。

【AHOI2013】差異

給定一個字串 \(S\),設 \(T_i\) 表示 \(S\) 的一個從第 \(i\) 個字元開始的字尾 \(S[i:]\)。求:\(\sum_{1\le i<j\le n} \Big(|T_i| + |T_j| - \text{LCP}(T_i,T_j)\Big)\) 的值(\(\text{LCP}(A,B)\) 表示 \(A,B\) 的最長公共字首長)。

首先將 \(S\) 翻轉,字尾變成字首,最長公共字首變最長公共字尾。

這裡涉及到一個關於 Parent Tree 的一個很重要的性質,兩個字首的最長公共字尾為兩字首對應的狀態 \(i, j\) 在 Parent Tree 上的最近公共祖先 \(\text{LCA}(i,j)\) 可以表示的最長串 (\(\text{longest}\))。因為跳 \(\text{link}\) 相當於壓縮地縮減字首,恰好縮減到一定程度兩者一樣了,自然也在一個等價類(狀態)中了。

考慮到一個狀態 \(x\) 到根 \(q_0\) 的路徑所有可表示長度區間連一起剛好就無縫銜接成了 \([0,\text{len}(x)]\),也就是說跳 \(\text{link}\) 遍歷的長度連續。那麼設 Parent Tree 上一條樹邊 \(\text{link}(x)\leftrightarrow x\) 的邊權為 \(\text{len}(x) - \text{len}(\text{link}(x))\),那麼答案就是任意兩個字首對於的狀態的樹上路徑長度和。

可以樹形 dp,但是直接考慮每條邊的貢獻更簡單(\(\text{siz}\) 表示 \(\text{endpos}\) 集大小):

\[\sum_{x\ne q_0} \text{siz}(x)\times (n-\text{siz}(x)) \times (\text{len}(x)-\text{len}(\text{link}(x))) \]

【HAOI2016】找相同字元

給定兩個字串,求出在兩個字串中各取出一個子串使得這兩個子串相同的方案數。兩個方案不同當且僅當這兩個子串中有一個位置不同。

首先對第一個串建 SAM,然後第二個在上面跑。對於 SAM 上每個狀態 \(x\),若其被匹配到了 \(k\) 次,那麼表明 \(x\) 可以表示的所有子串都將對答案貢獻 \(k\) 次。於是用一個 \(\text{vis}\) 記錄這個次數。

但並不是一個狀態的所有子串都匹配的上,因為在第二個串上匹配的長度可能不夠長。那麼再記錄一個 \(\text{cnt}(x)\) 表示狀態 \(x\) 的總貢獻。在跑的時候先記錄剩餘可能無法對當前狀態的所有子串都產生貢獻的殘餘計入 \(\text{cnt}\),對於一遍拓撲上來:

\[\forall\ \text{link}(y)=x :\begin{cases} \text{vis}(y) \to \text{vis}(x) \\ \text{vis}(y) \times (\text{len}(x)-\text{len}(\text{link}(x))) \to \text{cnt}(x) \end{cases} \]

最後 \(\sum_{x} (\text{cnt}(x)\times |\text{endpos}(x)|)\) 即為答案。

【NOI2015】品酒大會

給定一個字串 \(S(|S|=n)\),以及一個數列 \(a_1, a_2, \cdots, a_n\)(可能 \(< 0\))。規定第 \(p, q\) 個位置為 \(r-\)相似的,當且僅當 \(S[p:p+r-1]=S[q:q+r-1]\)。任意兩個不同位置的字元都是 \(0-\)相似的。\(\forall r\in[0, n):\) 求出有幾對字元是 \(r-\)相似的,以及所有 \(r-\)相似的位置對對應 \(a\) 的值的乘積的最大值。

很顯然如果 \(p,q\)\(r-\)相似的且不是 \((r+1)-\)相似的,那麼說明 \(\text{LCP}(S[p:],S[q:])=r\)

於是可以沿用“差異”那題的思路,翻轉 \(S\) 後轉化為樹上 \(\text{LCA}\) 問題。很容易求出每個 \(r\),是 \(r-\)相似但不是 \((r+1)-\)相似的個數,那麼字尾和一波就完事了(對於每個狀態 \(x\) 只在 \(\text{len}(x)\) 除更新答案接下來,原因同“SPOJ NSUBSTR”)。

第二問比較複雜,這就不得不建出 Parent Tree 做樹形 dp 了,否則容易出錯。很顯然就是子樹內選兩個值乘起來最大,那麼考慮維護最大次大值。然而有負數就很煩,還得維護最小次小值。然後最大乘積就是 \(\max(\text{最大}\times\text{次大},\text{最小}\times\text{次小})\)。然後也是隻要更新一個狀態的最長串,最後字尾 \(\max\) 即可。

【SPOJ REPEATS】repeats

求字串 \(S\) 連續出現次數最多的子串的連續出現次數。

若兩個相同並在部分重疊的子串,長度為 \(l\),終止位置分別為 \(x, y(y > x)\),那麼可以肯定除了尾部會斷開其他一定出現了迴圈,次數顯然是 \(\left\lfloor\frac{l-(y-x)}{y-x}\right\rfloor\),向下取整正好去除了尾部可能斷開的迴圈。

這是兩個相同的子串,那麼在 SAM 上也對應相同的狀態,\(x, y\) 只是 \(\text{endpos}\) 集中不同的兩個元素。對於 SAM 上這個狀態而言,若其 \(\text{endpos}\) 集合中元素不少於 \(2\),並且集合中兩兩元素做差得到最小的絕對值為 \(g\),那麼對答案的貢獻即為 \(\left\lfloor\frac{\text{len}+g}{g}\right\rfloor\)。可見我們需要求出整個 \(\text{endpos}\) 集合,以及最小間隔。

那麼線段樹合併或者平衡樹啟發式合併都是可以的。可能直接 std::set 更好寫?