目錄c++
此篇博客大部份內容來自於 hihoCoder , 藉此學習 !! (侵刪) 主要是上面講的通俗易懂qwqgit
本文只是將其用更好的格式進行展示,但願對讀者有幫助。算法
並且之後博客的 markdown 風格會進行改變qwq 主要是找到了新的 typora 用法 2333數組
不想看那麼長的講解,能夠直接先跳到後面的代碼再回頭看。markdown
對於一個字符串 \(S\) ,它對應的後綴自動機是一個最小的肯定有限狀態自動機( \(DFA\) ),接受且只接受 \(S\) 的後綴。函數
好比對於字符串 \(S=\underline{aabbabd}\),它的後綴自動機是學習
其中 紅色狀態 是終結狀態。你能夠發現對於S的後綴,咱們均可以從S出發沿着字符標示的路徑( 藍色實線 )轉移,最終到達終結狀態。ui
特別的,對於S的子串,最終會到達一個合法狀態。而對於其餘不是S子串的字符串,最終會「無路可走」。spa
咱們知道 \(SAM\) 本質上是一個 \(DFA\) ,\(DFA\) 能夠用一個五元組 <字符集、狀態集、轉移函數、起始狀態、終結狀態集> 來表示。至於那些 綠色虛線 雖然不是DFA的一部分,倒是SAM的重要部分,有了這些連接 \(SAM\) 是如虎添翼,咱們後面再細講。.net
其中比較重要的是 狀態集 和 轉移函數 .
首先咱們先介紹一個概念 子串的結束位置 集合 \(endpos\) 。
對於 \(S\) 的一個子串 \(s\) ,\(endpos(s) = s\) 在 \(S\) 中全部出現的結束位置集合。
仍是以 \(S=\underline{aabbabd}\) 爲例,\(endpos(\underline{ab}) = \{3, 6\}\) ,由於 \(\underline{ab}\) 一共出現了 \(2\) 次,結束位置分別是 \(3\) 和 \(6\) 。同理 \(endpos(\underline{a}) = \{1, 2, 5\}\) , \(endpos(\underline{abba}) = \{5\}\) 。
咱們把 \(S\) 的全部子串的 \(endpos\) 都求出來。若是兩個子串的 \(endpos\) 相等,就把這兩個子串歸爲一類。最終這些 \(endpos\) 的等價類就構成的 \(SAM\) 的狀態集合。
令 \(s_1,s_2\) 爲 \(S\) 的兩個子串 ,不妨設 \(|s_1|\le|s_2|\) (咱們用 \(|s|\) 表示 \(s\) 的長度 ,此處等價於 \(s_1\) 不長於 \(s_2\) )。則 \(s_1\) 是 \(s_2\) 的後綴當且僅當 \(endpos(s_1) \supseteq endpos(s_2)\) ,\(s_1\) 不是 \(s_2\) 的後綴當且僅當 \(endpos(s_1) \cap endpos(s_2) = \emptyset\) 。
這個證實是很顯然的 :
首先證實 \(s_1\) 是 \(s_2\) 的後綴 \(\Rightarrow\) \(endpos(s_1) \supseteq endpos(s_2)\) 。
由於每次出現 \(s_2\) 時候,\(s_1\) 必定會伴隨出現。而後證實 \(endpos(s_1) \supseteq endpos(s_2)\) \(\Rightarrow\) \(s_1\) 是 \(s_2\) 的後綴 。顯然 \(endpos(s_2) \not = \emptyset\) ,那麼意味着每次 \(s_2\) 結束的時候 \(s_1\) 也會結束,且 \(|s_1| \le |s_2|\) ,那麼顯然成立。
因此這兩個互爲充要條件。那麼 \(s_1\) 不是 \(s_2\) 的後綴當且僅當 \(endpos(s_1) \cap endpos(s_2) = \emptyset\) 就是其中的推論了,後者是前者的必要條件。
\(SAM\) 中的一個狀態包含的子串都具備相同的 \(endpos\),它們都互爲後綴。
其中一個狀態指的是從起點開始到這個點的全部路徑組成的子串的集合。
例如上圖中狀態 \(4\) 爲 \(\{\underline{bb},\underline{abb},\underline{aabb}\}\) 。
咱們用 \(substrings(st)\) 表示狀態 \(st\) 中包含的全部子串的集合,\(longest(st)\) 表示 \(st\) 包含的最長的子串,\(shortest(st)\)表示\(st\)包含的最短的子串。
例如對於狀態 \(7\) ,\(substring(7)=\{\underline{aabbab},\underline{abbab},\underline{bbab},\underline{bab}\}\),\(longest(7)=\underline{aabbab}\),\(shortest(7)=\underline{bab}\) 。
那麼有 對於一個狀態 \(st\) ,以及任意 \(s\in substrings(st)\) ,都有 \(s\) 是 \(longest(st)\) 的後綴。
證實比較容易,由於 \(endpos(s)=endpos(longest(st)) ~|s| \le |st|\) ,因此 \(endpos(s) ⊇ endpos(longest(st))\) ,根據咱們剛纔證實的結論有 \(s\) 是 \(longest(st)\) 的後綴。
對於一個狀態 \(st\) ,以及任意的 \(longest(st)\) 的後綴 \(s\) ,若是 \(s\) 的長度知足:\(|shortest(st)| \le |s| \le |longsest(st)|\) ,那麼 \(s \in substrings(st)\) 。
實際上是定義而後很顯然?證實有:\(|shortest(st)| \le|s|\le|longsest(st)|\)> ,因此\(endpos(shortest(st)) \supseteq endpos(s) \supseteq endpos(longest(st))\) ,又 \(endpos(shortest(st))=endpos(longest(st))\) 因此 \(endpos(shortest(st)) = endpos(s) = endpos(longest(st))\) ,因此 \(s\in substrings(st)\) 。
也就是說 \(substrings(st)\) 包含的是 \(longest(st)\) 的一系列 連續 後綴。
例如 狀態 \(7\) 中包含的就是 \(\underline{aabbab}\) 的長度分別是 \(6,5,4,3\) 的後綴;狀態 \(6\) 包含的是 \(\underline{aabba}\) 的長度分別是 \(5,4,3,2\) 的後綴。
前面咱們講到 \(substrings(st)\) 包含的是 \(longest(st)\) 的一系列 連續 後綴。這連續的後綴在某個地方會「斷掉」。
好比狀態 \(7\) ,包含的子串依次是 \(\underline{aabbab},\underline{abbab},\underline{bbab},\underline{bab}\) 。按照連續的規律下一個子串應該是 \(\underline{ab}\) ,可是 \(\underline{ab}\) 沒在狀態 \(7\) 裏。
這是爲何呢?
\(\underline{aabbab},\underline{abbab},\underline{bbab},\underline{bab}\) 的 \(endpos\) 都是 \(\{6\}\) ,下一個 \(\underline{ab}\) 固然也在結束位置 \(6\) 出現過,可是 \(\underline{ab}\) 還在結束位置 \(3\) 出現過,因此 \(\underline{ab}\) 比 \(\underline{aabbab},\underline{abbab},\underline{bbab},\underline{bab}\) 出現次數更多,因而就被分配到一個新的狀態中了。
當 \(longest(st)\) 的某個後綴 \(s\) 在新的位置出現時,就會「斷掉」,\(s\) 會屬於新的狀態。
好比上例中 \(\underline{ab}\) 就屬於狀態 \(8\) ,\(endpos(\underline{ab})=\{3,6\}\) 。當咱們進一步考慮 \(\underline{ab}\) 的下一個後綴 \(\underline{b}\) 時,也會遇到相同的狀況:\(\underline{b}\) 還在新的位置 \(4\) 出現過,因此 \(endpos(\underline{b})=\{3,4,6\}\) ,\(\underline{b}\) 屬於狀態 \(5\) 。在接下去處理 \(\underline{b}\) 的後綴咱們會遇到空串, \(endpos(\underline{})= \{0,1,2,3,4,5,6\}\) ,狀態是起始狀態 \(S\) 。
因而咱們能夠發現一條狀態序列: \(7 \to 8 \to 5 \to S\) 。這個序列的意義是 \(longest(7)\) 即 \(\underline{aabbab}\) 的後綴依次在狀態 \(7,8,5,S\) 中。咱們用後綴連接 \(Suffix Link\) 這一串狀態連接起來,這條 \(link\) 就是上圖中的綠色虛線。
後面這個會有妙用qwq
最後咱們來介紹 \(SAM\) 的轉移函數。對於一個狀態 \(st\) ,咱們首先找到從它開始下一個遇到的字符多是哪些。咱們將 \(st\) 遇到的下一個字符集合記做 \(next(st)\) ,有 \(next(st) = \{S[i+1] | i \in endpos(st)\}\) 。例如 \(next(S)=\{S[1], S[2], S[3], S[4], S[5], S[6], S[7]\}=\{a, b, d\}\) ,\(next(8)=\{S[4], S[7]\}=\{b, d\}\) 。
對於一個狀態 \(st\) 來講和一個 \(next(st)\) 中的字符 \(c\) ,你會發現 \(substrings(st)\) 中的全部子串後面接上一個字符 \(c\) 以後,新的子串仍然都屬於同一個狀態。
好比對於狀態 \(4\) ,\(next(4)=\{a\}\) ,\(\underline{aabb},\underline{abb},\underline{bb}\) 後面接上字符 \(\underline{a}\) 獲得 \(\underline{aabba},\underline{abba},\underline{bba}\) ,這些子串都屬於狀態 \(6\) 。
因此咱們對於一個狀態 \(st\) 和一個字符 \(c\in next(st)\) ,能夠定義轉移函數 \(trans(st, c) = \{x | longest(st) + c \in substrings(x) \}\) 。換句話說,咱們在 \(longest(st)\)(隨便哪一個子串都會獲得相同的結果)後面接上一個字符 \(c\) 獲得一個新的子串 \(s\) ,找到包含 \(s\) 的狀態 \(x\) ,那麼 \(trans(st, c)\) 就等於\(x\) 。
\(SAM\) 有 \(O(|S|)\) 的構造方法,接下來咱們講講如何構造。
首先爲了實現 \(O(|S|)\) 的構造,對於每一個狀態確定不能保存太多數據。例如 \(substrings(st)\) 確定沒法保存下來了。
對於狀態 \(st\) 咱們只保存以下數據:
數據 | 含義 |
---|---|
\(maxlen[st]\) | \(st\) 包含的最長子串的長度 |
\(minlen[st]\) | \(st\) 包含的最短子串的長度 |
\(trans[st][1..c]\) | \(st\) 的轉移函數, \(c\) 爲字符集大小 |
\(link[st]\) | \(st\) 的後綴連接 |
其次,咱們使用增量法構造 \(SAM\) 。咱們從初始狀態開始,每次考慮添加一個字符 \(S[1],S[2],...,S[N]\) ,依次構造能夠識別 \(S[1], S[1..2], S[1..3], \cdots ,S[1..N]=S\) 的 \(SAM\) 。
假設咱們已經構造好了 \(S[1..i]\) 的 \(SAM\) 。這時咱們要添加字符 \(S[i+1]\) ,因而咱們新增了 \(i+1\) 個 \(S[i+1]\) 的後綴要識別:\(S[1..i+1], S[2..i+1], ... S[i..i+1], S[i+1]\) 。 考慮到這些新增狀態分別是從 \(S[1..i], S[2..i], S[3..i], \cdots , S[i], \underline{}\) (空串)經過字符 \(S[i+1]\) 轉移過來的,因此咱們還要對 \(S[1..i], S[2..i], S[3..i], \cdots , S[i], \underline{}\) (空串) 對應的狀態們增長相應的轉移。
咱們假設 \(S[1..i]\) 對應的狀態是 \(u\) ,等價於 \(S[1..i]\in substrings(u)\) 。根據上面的討論咱們知道 \(S[1..i], S[2..i], S[3..i], ... , S[i], \underline{}\) (空串)對應的狀態們剛好就是從 \(u\) 到初始狀態 \(S\) 的由 \(Suffix Link\) 鏈接起來路徑上的全部狀態,不妨稱這條路徑(上全部狀態集合)是 \(suffix-path(u\to S)\) 。
這個也就是說,對於 \(S[1..i] = longest(u) \in substrings(u)\) 對於其餘 \(s'\) 爲 \(longest(u)\) 的後綴 要麼存在於 \(u\) 這個狀態中,要麼存在於前面的 \(SuffixLink\) 鏈接的狀態中。
顯然至少 \(S[1..i+1]\) 這個子串不能被之前的 \(SAM\) 識別,因此咱們至少須要添加一個狀態 \(z\) ,\(z\) 至少包含\(S[1..i+1]\) 這個子串。
首先考慮一種最簡單的狀況:對於 \(suffix-path(u \to S)\) 的任意狀態 \(v\) ,都有 \(trans[v][S[i+1]]=NULL\) 。這時咱們只要令 \(trans[v][S[i+1]]=z\) ,而且令 \(link[st]=S\) 便可。
例如咱們已經獲得了 \(\underline{aa}\) 的 \(SAM\) ,如今但願構造 \(\underline{aab}\) 的 \(SAM\) 。就以下圖所示:
此時 \(u=2,z=3\) , \(suffix-path(u\to S)\) 是 桔色狀態 組成的路徑 \(2-1-S\) 。而且這 \(3\) 個狀態都沒有對應字符 \(b\) 的轉移。因此咱們只要添加紅色轉移 \(trans[2][b]=trans[1][b]=trans[S][b]=z\) 便可。固然也不要忘了 \(link[3]=S\) 。
還有一種難一點的狀況爲:\(suffix-path(u\to S)\) 上有一個節點 \(v\) ,使得 \(trans[v][S[i+1]]\not =NULL\) 。
咱們如下圖爲例,假設咱們已經構造 \(\underline{aabb}\) 的 \(SAM\) 如圖,如今咱們要增長一個字符 \(a\) 構造 \(\underline{aabba}\) 的 \(SAM\) 。
這時 \(u=4,z=6,suffix-path(u\to S)\) 是 桔色狀態 組成的路徑 \(4-5-S\) 。對於狀態 \(4\) 和狀態 \(5\) ,因爲它們都沒有對應字符 \(a\) 的轉移,因此咱們只要添加紅色轉移\(trans[4][a]=trans[5][a]=z=6\) 便可。但此時 \(trans[S][a]=1\) 已經存在了。
不失通常性,咱們能夠認爲在 \(suffix-path(u\to S)\) 遇到的第一個狀態 \(v\) 知足 \(trans[v][S[i+1]]=x\) 。這時咱們須要討論 \(x\) 包含的子串的狀況。
若是 \(x\) 中包含的最長子串就是 \(v\) 中包含的最長子串接上字符\(S[i+1]\) ,等價於 \(maxlen(v)+1=maxlen(x)\) 。這種狀況比較簡單,咱們只要增長 \(link[z]=x\) 便可。
好比在上面的例子裏,\(v=S, x=1\) ,\(longest(v)\) 是空串,\(longest(1)=\underline{a}\) 就是 \(longest(v)+\underline{a}\) 。
咱們將狀態 \(6\) \(link\) 到狀態 \(1\) 就好了。由於此時 \(z\) 只缺乏了這個 \(suffix-path(x \to S)\) 的狀態。
若是 \(x\) 中包含的最長子串 不是 \(v\) 中包含的最長子串接上字符 \(S[i+1]\) ,等價於 \(maxlen(v)+1 < maxlen(x)\) ,這種狀況最爲複雜。
不失通常性,咱們用下圖表示這種狀況,這時增長的字符是 \(c\) ,狀態是 \(z\) 。
在 \(suffix-path(u\to S)\) 這條路徑上,從 \(u\) 開始有一部分連續的狀態知足 \(trans[u..][c]=NULL\) ,對於這部分狀態咱們只需增長 \(trans[u..][c]=z\) 。緊接着有一部分連續的狀態 \(v..w\) 知足\(trans[v..w][c]=x\) ,而且 \(longest(v)+c\) 不等於 \(longest(x)\) 。
這時咱們須要從 \(x\) 拆分出新的狀態 \(y\) ,而且把原來 \(x\) 中長度小於等於 \(longest(v)+c\) 的子串分給 \(y\) ,其他子串留給 \(x\) 。同時令 \(trans[v..w][c]=y\) ,\(link[y]=link[x], ~link[x]=link[z]=y\) 。
也就是 \(y\) 先繼承 \(x\) 的 \(link\) ,而且 \(x,z\) 前面斷開的 \(substrings\) 就存在於 \(y\) 中了。
好像比較複雜。咱們來舉個例子。假設咱們已經構造 \(\underline{aab}\) 的 \(SAM\) 如圖,如今咱們要增長一個字符\(b\) 構造 \(\underline{aabb}\) 的 \(SAM\) 。
當咱們處理在 \(suffix-path(u\to S)\) 上的狀態 \(S\) 時,遇到 \(trans[S][b]=3\) 。而且 \(longest(3)=\underline{aab}\) ,\(longest(S)+ \underline{b}= \underline{b}\) ,二者不相等。其實不相等意味增長了新字符後 \(endpos(\underline{aab})\) 已經不等於 \(endpos(\underline{b})\) ,勢必這兩個子串不能同屬一個狀態 \(3\) 。這時咱們就要從 \(3\) 中新拆分出一個狀態 \(5\) ,把 \(\underline{b}\) 及其後綴分給 \(5\) ,其他的子串留給 \(3\) 。同時令 \(trans[S][c]=5, link[5]=link[3]=S, link[3]=link[6]=5\) 。
到此整個構造算法所有結束。
不難發現這個的時間複雜度只與 狀態以及轉移的數量 有關。
咱們考慮分析這兩個部分。這部分證實來自大佬 DZYO的博客 。
由長度爲 \(n\) 的字符串 \(s\) 創建的後綴自動機的狀態個數不超過 \(2n-1\)(對於 \(n\ge 3\) )。
證實:上面描述的算法證實了這一性質(最初自動機包含一個初始節點,第一步和第二步都會添加一個狀態,餘下的 \(n-2\) 步每步因爲須要分割,至多增長兩個狀態)。
因此就是 \(1+2+(n-2) \times 2 = 2n-1\) 了。
有趣的是,這一上限沒法被改善,即存在達到這一上限的例子: \(\underline{abbb...}\) 。每次添加都須要分割。
由長度爲 \(n\) 的字符串 \(s\) 創建的後綴自動機中,轉移的數量不超過 \(3n-4\) (對於 \(n\ge 3\) )。
證實: 咱們計算 連續的 轉移個數。考慮以 \(S\) 爲初始節點的自動機的最長路徑樹。這棵樹將包含全部連續的轉移,樹的邊數比結點個數小 \(1\) ,這意味着連續的轉移個數不超過 \(2n-2\) 。
咱們再來計算 不連續 的轉移個數。考慮每一個不連續轉移;假設該轉移——轉移 \((p,q)\) ,標記爲 \(c\) 。對自動機運行一個合適的字符串 \(u+c+w\) ,其中字符串 \(u\) 表示從初始狀態到 \(p\) 通過的最長路徑,\(w\) 表示從 \(q\) 到任意終止節點通過的最長路徑。
一方面,對全部不連續轉移,字符串 \(u+c+w\) 都是不一樣的(由於字符串 \(u\) 和 \(w\) 僅包含連續轉移)。另外一方面,每一個這樣的字符串 \(u+c+w\) ,因爲在終止狀態結束,它必然是完整串 \(s\) 的一個後綴。因爲 \(s\) 的非空後綴僅有 \(n\) 個,而且完整串 \(s\) 不能是某個 \(u+c+w\) (由於完整串 \(s\) 匹配一條包含 \(n\) 個連續轉移的路徑),那麼不連續轉移的總共個數不超過 \(n-1\) 。
有趣的是,仍然存在達到轉移個數上限的數據:\(\underline{abbb...bbbc}\) 。
這個證實其實我是沒太懂的。。記下結論吧。
咱們令 \(id\) 爲此次插入字符的編號,\(trans,maxlen,link\) 意義同上。\(Last\) 爲上次最後插入的狀態的編號,\(Size\) 爲當前的狀態總數,\(clone\) 爲複製節點即上文的 \(y\) 。 具體來講以下代碼所示:
\(minlen\) 能夠最後計算 ,由於咱們是從 \(link\) 處斷開的,因此顯然有 \(minlen[i] = maxlen[link[i]]+1\) 。
struct Suffix_Automata { int maxlen[Maxn], trans[Maxn][26], link[Maxn], Size, Last; Suffix_Automata() { Size = Last = 1; } inline void Extend(int id) { int cur = (++ Size), p; maxlen[cur] = maxlen[Last] + 1; for (p = Last; p && !trans[p][id]; p = link[p]) trans[p][id] = cur; if (!p) link[cur] = 1; else { int q = trans[p][id]; if (maxlen[q] == maxlen[p] + 1) link[cur] = q; else { int clone = (++ Size); maxlen[clone] = maxlen[p] + 1; Cpy(trans[clone], trans[q]); link[clone] = link[q]; for (; p && trans[p][id] == q; p = link[p]) trans[p][id] = clone; link[cur] = link[q] = clone; } } Last = cur; } } T;
其實這個能夠用後綴數組作,具體來講,答案就是 \(\displaystyle \sum_{i=1} ^ n (n - sa[i] + 1) - height[i]\) 。咱們考慮 \(sa\) 相鄰兩個後綴。首先多出了 \((n - sa[i] + 1)\) 個後綴,而後 \(LCP\) 長度爲 \(height[i]\) 的子串重複計算過,減去就好了。
用 \(SAM\) 的話,其實就是統計全部狀態包含的子串總數,也就是 \(\displaystyle \sum_{i=1}^{Size} maxlen[i] - minlen[i]+1\) ,建完直接算就好了。注意前面講過的 \(minlen[i] = maxlen[link[i]]+1\) 。
咱們首先考慮一個子串出現的次數,不難發現就是它 \(endpos\) 集合的大小。因此咱們當前須要計算的就是 \(\forall st, |endpos(st)|\) 的大小。若是咱們每次構建時候維護這個的話,每次須要跳完整個 \(suffix-path(u\to S)\) ,對於這上面的全部節點加一(這是由於後綴路徑上的全部點都具備新加狀態的 \(endpos\) ,總時間複雜度能達到 \(O(|S|^2)\) ,可是對於隨機數據表現優秀)。咱們先構造完 \(SAM\) 最後再算答案。咱們單獨把它全部的後綴路徑拿出來看一下是什麼狀況。
咱們以最開始的 \(SAM\) 爲例,它後綴連接構成的圖以下:
不難他的後綴連接組成了一個 \(DAG\) 圖。而且它反向建那麼就是一顆以 \(S\) 爲根的樹(由於除了 \(S\) 每一個點有且僅有一個出邊,而且不可能存在環,由於 \(maxlen[link[i]] < maxlen[i]\) ),咱們稱之爲後綴樹。
前面講過了咱們每次是暴力把路徑上的全部點權值 \(+1\) 。咱們就能轉化成 \(DAG\) 每個點對於它能走的路徑上的全部點 \(+1\) ,這個直接考慮在 \(DAG\) 圖上進行拓撲 \(dp\) 就好了。
但注意 \(clone\) 的節點是不能對它到 \(S\) 的路徑上有單獨貢獻的,由於它的貢獻會在它的本體上計算一遍。
而後這題是要計算對於全部 \(i\) 長度爲 \(i\) 子串個數,那麼不難發現一個狀態 \(st\) 包含的是長度爲 \([minlen(st), maxlen(st)]\) 的子串,那麼它對於 \(minlen(st) \le k \le maxlen(st)\) 的長度的答案具備貢獻。這個咱們打個區間取 \(max\) 就好了。這樣要寫一個線段樹比較麻煩,但咱們發現對於長度更大 \(ans\) 我當前確定也是可使用的,一開始把標記打在 \(maxlen\) 上,直接最後倒着取 \(max\) 就好了。
至此這道題就作完啦。複雜度爲 \(O(n)\) 比排序 \(len\) 的複雜度 \(O(n \log n)\) 要優秀。
vector<int> G[Maxn]; int indeg[Maxn]; void Build() { For (i, 1, Size) G[i].push_back(link[i]), ++ indeg[link[i]]; } void Topo() { queue<int> Q; Build(); For (i, 0, Size) if (!indeg[i]) Q.push(i); while (!Q.empty()) { int u = Q.front(); Q.pop(); for (int v : G[u]) { val[v] += val[u]; if (!(-- indeg[v])) Q.push(v); } } For (i, 1, Size) chkmax(Tag[maxlen[i]], val[i]); Fordown (i, n, 1) chkmax(Tag[i], Tag[i + 1]); }
此題就是要統計全部本質不一樣的子串權值和,對於每一個子串權值定義就是它在十進制下的值。由於每一個數都是從前日後構成的,而且 \(SAM\) 上每一個狀態的 \(substrings\) 是從起點開始的路徑構成的單詞集合。
正向的轉移函數 \(trans[u][1..c]\) 是一個 \(DAG\) 圖。
由於狀態有限,因此不可能存在環使得狀態無限。
不難考慮用正向拓撲 \(dp\) 求解這個值。令 \(dp_{i}\) 爲狀態 \(i\) 全部 \(substrings(i)\) 的權值和,那麼顯然有 \(\displaystyle dp_{v}=\sum_{trans[u][id]} dp[u] \times 10 + id\) . 但這樣顯然會錯... 由於一個狀態可能有不少子串加上了 \(id\) 這個值,但咱們只加上了一個,因此咱們記下每一個狀態具備的子串個數 \(tot_i\) 。那麼有 \(\displaystyle tot_v = \sum_{trans[u][id]}tot_u\) 。又有 \(\displaystyle dp_v = \sum_{trans[u][id]}dp[u] \times 10 + id \times tot_u\) 。
可是這個是有許多串一塊兒詢問答案,能夠用 廣義後綴自動機 來解決。
但其實這題咱們能夠用當初作後綴數組題的一些思想,咱們對於許多子串在中間加入一些字符例如 \(\underline{:}\) (字符集大小 \(+1\) )將其隔開,而後每次統計的時候不能統計中間具備 \(\underline{:}\) 的字符,對於這些枚舉的邊爲這些轉移的,咱們就不轉移 \(dp,tot\) 就能夠了。
int val[Maxn], indeg[Maxn], tot[Maxn], n; void Get_Val() { queue<int> Q; Q.push(1); tot[1] = 1; For (i, 1, Size) For (j, 0, spc) ++ indeg[trans[i][j]]; while (!Q.empty()) { int u = Q.front(); Q.pop(); For (i, 0, spc) { int v = trans[u][i]; if (!v) continue ; if (i != 10) { (tot[v] += tot[u]) %= Mod; (val[v] += val[u] * 10ll % Mod + 1ll * i * tot[u] % Mod) %= Mod; } if (!(-- indeg[v])) Q.push(v); } } }
這個比較巧妙qwq ,首先先講如何求 兩個串的最長公共子串 \((LCS)\) 注意此處不是最長公共前綴 \((LCP)\) 。
假設咱們當前有兩個串 \(S\) 與 \(T\) ,求它們的 \(LCS\) 咱們考慮先把 \(S\) 的 \(SAM\) 建出來。
而後對於 \(T\) 的每個位置 \(T[i]\) 計算出以 \(T[i]\) 爲結尾的子串與 \(S\) 的 \(LCS\) 。
好比對於 \(S=\underline{aabbabd}, T=\underline{abbabb}\) 。獲得的狀況以下:
S: aabbabd T: abbabb 1: a 2: ab 3: abb 4: abba 5: abbab 6: abb這個如何求呢?
首先,對於每個 \(T[i]\) 咱們記兩個數據 \(u, l\) 分別表明當前 \(LCS\) 所在的 \(SAM\) 狀態以及它在原串的長度。
咱們假設咱們已經獲得了 \(T[i-1]\) 的 \(u,l\) ,如今咱們要求 \(T[i]\) 的 \(u',l'\) 。討論幾種狀況就好了。
\(trans[u][T[i]] =v,v \not = NULL\) 。這種就很顯然了,直接向後匹配一位。\(u' = v, l' = l +1\) 。
\(trans[u][T[i]]=NULL\) 。這種咱們能夠用相似 \(KMP\) 和 \(AC\) 自動機的方法跳 \(fail\) ,此處咱們的 \(Suffix~Link\) 至關於 \(fail\) ,由於每次失配後咱們只須要找它的一個前綴使得恰好匹配。咱們有以前的結論 \(longest(st)\) 的前綴必在 \(Suffix-Path(u \to S)\) 的狀態上。
因此咱們每次向前跳 \(Suffix-Path(u \to S)\) 上的點 \(q\) ,直到找到第一個 \(trans[q][T[i]] = v, v \not = NULL\) ,此時 \(u'=v, l' = maxlen[q]+1\) 。由於此時 \(maxlen[q]\) 是恰好能知足的前綴的長度。
若是整條鏈不存在那就令 \(u'=s, l'=0\) 。
這樣就是 \(O(|S| + |T|)\) 的複雜度了,輕鬆愉悅。
有了這個後就很好作了。循環的串,不難想到拆壞爲鏈,也就是說咱們將要查詢的串倍長去裏面匹配。
假設對於 \(\underline{aab}\) 咱們將其變成 \(\underline{aabaab}\) 而後對於其中每個位置,若是與原串獲得的 \(LCS\) 的長度不小於這個串的長度 \((l \ge |T|)\) ,那麼以這個點結尾的循環串就會在原串中出現。仍是剛剛那個例子,假設原串是 \(\underline{abaaa}\) ,對於查詢串位置爲 \(5\) 的地方與原串 \(LCS\) 長度爲 \(4\) 那麼對於 \(\underline{baa}\) 必在原串中出現過,它出現的次數也就是求 \(LCS\) 時候狀態 \(u\) 出現的次數。
狀態出現次數能夠用前面講過的計數方法來求,求 \(LCS\) 的狀態也能夠按前面來求。但這樣有兩個問題……
int version[Maxn]; ll Calc(char *str, int num) { ll res = 0; int u = 1, lcs = 0, len = strlen(str + 1), bas = len >> 1; For (i, 1, len) { int id = str[i] - 'a'; if (trans[u][id]) u = trans[u][id], ++ lcs; else { for (; u && !trans[u][id]; u = link[u]) ; if (!u) { u = 1; lcs = 0; } else lcs = maxlen[u] + 1, u = trans[u][id]; } if (lcs >= bas) { while (maxlen[link[u]] >= bas) lcs = maxlen[u = link[u]]; if (version[u] != num) version[u] = num, res += times[u]; } } return res; }
首先認真讀題。
給你兩個串 \(A,B\) 。而後天天你要和別人博弈,博弈規則以下:
而後天天你能夠制定這兩個子串,但任意兩天不能重複,字典序從小到大制定(先比 \(A\) 再比 \(B\) )。且你須要一直贏 \(k\) 天,問第 \(k\) 天你給出的字符串是什麼。若是無解輸出 \(NO\) 。
\((|A|,|B| \le 10^5, k \le 10^{18})\)
咱們每次末尾添加一個字符並還是原串的一個子串的操做就至關於在 \(SAM\) 按照 \(trans\) 移動到後一個節點。而後沒有轉移了就爲敗態。因爲 \(trans\) 是個 \(DAG\) 圖,咱們這個至關於在 \(DAG\) 上進行移動,咱們能夠直接用組合遊戲 \((nim)\) 的結論,也就是 \(SG\) 函數。
對於 \(DAG\) 上任意一個點的 \(SG\) 值爲 \(mex_{v\in G[u]} \{SG[v]\}\) 。 \(mex \{S\}\) 定義爲 \(S\) 集合中第一個未出現的天然數。而後必敗態的 \(SG\) 值爲 \(0\) 。若是初始狀態的 \(SG\) 值不爲 \(0\) 先手必勝,不然必敗。
而後這是兩個獨立的遊戲,把它們合併的話就是它們全部的 \(SG\) 異或和不爲 \(0\) 先手必勝,不然必敗。
但此處是要求第 \(k\) 個可行的答案。那麼咱們只要首先在 \(A\) 的 \(trans\) 上按 \(a \to z\) 的順序走,每次走的時候只要保證接下來走的對應方案數足夠就好了。
那麼咱們須要統計一個這個東西 \(tot[u][i][0/1]\) 表示 \(u\) 這個狀態包含的子串爲前綴 \(SG\) 值 是/否 爲 \(i\) 的子串個數。
例如對於 \(\underline{ab}\) 來講,狀態 \(1\) 爲起點,它包含的子串爲 \(\underline{}\) (空串)。因此它爲前綴所包含的子串集合爲 \(\{\underline{}, \underline{a}, \underline{b}, \underline{ab}\}\) 。\(SG\) 值分別爲 \(\{2,1,0,0\}\) 因此它的 \(tot[1][2][1]=1,tot[1][2][0]=3\) 。
這個 \(tot\) 與 \(SG\) 能夠直接先求出 \(trans\) 的拓撲序,而後倒推就好了,這個比較容易推。而後咱們有了這個就很好作了。
不難發現 \(SG\) 值最多隻有 \(26\) 由於每一個點最多隻會有 \(26\) 個出邊,因此這些最多隻能從 \([0,25]\) 取值,也就是說這個點 \(SG\) 值最大爲 \(26\) 。
咱們首先肯定 \(A\) 的串應該是什麼,咱們從高到低依次枚舉每一位,判斷是否在須要走入其中。具體來講咱們假設當前到了 \(SAM\) 的第 \(u\) 個點須要取字典序第 \(k\) 小的字符串,在當前這個點的結束條件是 \(B.tot[A.SG[u]][S][0] \le k\) 也就是意味着對於這個點能取勝的總方案數是 \(B\) 中 \(SG\) 不和 \(A.SG[u]\) 相等的子串數。而後若是在當前節點結束不了,那麼咱們先減去這一部分的貢獻。而後枚舉接下來那一位,選擇這個節點的貢獻就是 \(\displaystyle \sum_{i=0}^{c+1} A.tot[v][i][1] \times B.tot[1][i][0]\) 也是就走完這一步後手必敗的方案數之和,而後判一下大小就好了。
接下來只須要肯定 \(B\) 串了,咱們只須要用以前最後肯定 \(A\) 串的 \(SG\) 函數去算就好了,具體見代碼。(彷佛寫的有點長。。。湊合看吧。。。)
時間複雜度 \(O((|A|+|B|)c)\) ,\(c\) 爲字符集大小。
#include <bits/stdc++.h> #define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i) #define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i) #define Set(a, v) memset(a, v, sizeof(a)) #define Cpy(a, b) memcpy(a, b, sizeof(a)) #define debug(x) cout << #x << ": " << x << endl using namespace std; inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;} inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;} typedef long long ll; inline ll read() { ll x = 0, fh = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48); return x * fh; } void File() { #ifdef zjp_shadow freopen ("1466.in", "r", stdin); freopen ("1466.out", "w", stdout); #endif } const int N = 1e5 + 1e3, Maxn = N << 1, spc = 25; struct Suffix_Automata { int trans[Maxn][spc + 1], maxlen[Maxn], minlen[Maxn], link[Maxn], Size, Last; Suffix_Automata() { Last = Size = 1; } inline void Extend(int id) { int cur = (++ Size), p; maxlen[cur] = maxlen[Last] + 1; for (p = Last; p && !trans[p][id]; p = link[p]) trans[p][id] = cur; if (!p) link[cur] = 1; else { int q = trans[p][id]; if (maxlen[q] == maxlen[p] + 1) link[cur] = q; else { int clone = (++ Size); maxlen[clone] = maxlen[p] + 1; Cpy(trans[clone], trans[q]); link[clone] = link[q]; for (; p && trans[p][id] == q; p = link[p]) trans[p][id] = clone; link[cur] = link[q] = clone; } } Last = cur; } int SG[Maxn], lis[Maxn], indeg[Maxn], cnt; ll tot[Maxn][spc + 2][2]; void Get_SG_Tot() { queue<int> Q; cnt = 0; Q.push(1); For (i, 1, Size) For (j, 0, spc) if (trans[i][j]) ++ indeg[trans[i][j]]; while (!Q.empty()) { int u; u = lis[++ cnt] = Q.front(); Q.pop(); For (i, 0, spc) { int v = trans[u][i]; if (!v) continue ; if (!(--indeg[v])) Q.push(v); } } bitset<spc + 2> App; Fordown (i, cnt, 1) { int u = lis[i]; App.reset(); For (j, 0, spc) { register int v = trans[u][j]; if (v) { App[SG[v]] = true; For (k, 0, spc + 1) tot[u][k][1] += tot[v][k][1]; } } for (int j = 0; ; ++ j) if (!App[j]) { SG[u] = j; break; } ll sum = 0; ++ tot[u][SG[u]][1]; For (i, 0, spc + 1) sum += tot[u][i][1]; For (i, 0, spc + 1) tot[u][i][0] = sum - tot[u][i][1]; } } void Out() { For (i, 1, Size) { debug(i); For (j, 0, 5) printf ("%lld%c", tot[i][j][1], j == jend ? '\n' : ' '); debug(SG[i]); } } } A, B; char ansa[N], ansb[N]; ll k; int Get_A(int u, int cur) { ll cnt = B.tot[1][A.SG[u]][0]; if (k <= cnt) return u; k -= cnt; For (i, 0, spc) { int v = A.trans[u][i]; if (!v) continue ; ll now = 0; For (i, 0, spc + 1) now += 1ll * A.tot[v][i][1] * B.tot[1][i][0]; if (now < k) k -= now; else { ansa[cur] = i + 'a'; return Get_A(v, cur + 1); } } return 0; } void Get_B(int u, int cur, int val) { k -= (val != B.SG[u]); if (!k) return ; For (i, 0, spc) { int v = B.trans[u][i]; if (!v) continue ; ll now = B.tot[v][val][0]; if (now < k) k -= now; else { ansb[cur] = i + 'a'; Get_B(v, cur + 1, val); return ; } } } char str[N]; int main () { File(); k = read(); scanf ("%s", str + 1); For (i, 1, strlen(str + 1)) A.Extend(str[i] - 'a'); A.Get_SG_Tot(); A.Out(); scanf ("%s", str + 1); For (i, 1, strlen(str + 1)) B.Extend(str[i] - 'a'); B.Get_SG_Tot(); int pos = Get_A(1, 1); if (!pos) return puts("NO"), 0; Get_B(1, 1, A.SG[pos]); printf ("%s\n", ansa + 1); printf ("%s\n", ansb + 1); return 0; }