我永遠喜歡資料結構。
大家好,我喜歡暴力資料結構,所以使用分塊透過了此題。
\(\color{maroon}*3400\) 是你的謊言。我覺得這題遠大於 CF1608G 啊。
洛谷題目傳送門
CodeForces 題目傳送門
給出 \(n\) 個點的樹,第 \(i\) 條邊上有字母 \(c_i\)。有 \(m\) 個字串 \(s_1\sim s_m\) 以及 \(q\) 組詢問。每次詢問給出 \(x,y,k\)。
記 \(\text{str}(x,y)\) 為 \(x,y\) 簡單有向路徑邊上的字母按順序拼接得到的字串,形式化地,若 \(x,y\) 簡單有向路徑上一共有 \(E\) 條邊,記 \(e_i\) 為 \(x,y\) 有向路徑上的第 \(i\) 條邊,則 \(\text{str}(x,y)=\overline{c_{e_1}c_{e_2}\dots c_{e_E}}\)。
求 \(s_k\) 在 \(\text{str}(x,y)\) 中出現了多少次。形式化地,求有多少個正整數 \(i\in[1,|str(x,y)|-|s_k|+1]\) 使得 \(\forall\,j\in[0,|s_k|-1]\bigcup \mathbb{Z},(s_k)_{i+j}=[\text{str}(x,y)]_{i+j}\)。
記 \(M=\sum\limits_{i=1}^m |s_i|\),\(n,m,M,q\le 10^5\)。
\(\text{3 s / 512 MB}\)。
約定:
-
本文中所有下標均從 \(1\) 開始。欽定 \(1\) 為根。用印表機字型(
\texttt
)表示具體的字元 / 字串。 -
預設 \(\mathcal{O}(n)=\mathcal{O}(m)=\mathcal{O}(M)=\mathcal{O}(q)\),\(\mathcal{O}(\sqrt{n})>\mathcal{O}\left(\log^2 n\right)\)。
-
記一個點 \(u\) 的父親為 \(\text{fa}_u\),深度(到根的邊數)為 \(\text{dep}_u\)。
-
\(x\rightsquigarrow y\) 表示 \(x\) 到 \(y\) 的簡單有向路徑,\(u\longleftrightarrow \text{fa}_u\) 這條邊上的字元為 \(\text{val}_u\)。特別地,\(\text{val}_1=\texttt{1}\)。
-
\(\text{lca}(x,y)\) 表示 \(x,y\) 兩點的最近公共祖先。\(\text{lcp}(x,y)\) 表示兩個字串 \(x,y\) 的最長公共字首。
-
記一個串 \(s\) 的反串為 \(s^R\)。形式化地,\(\left|s^R\right|=|s|\) 且 \(s^R_i=s_{|s|-i+1}\)。
-
\(\text{anc}(k,x)\) 表示 \(x\) 向上走 \(k\) 條邊到達的點,即 \(x\) 的樹上 \(k\) 級祖先。
-
若字元參與運算,則其值等於其 \(\text{ASCII}\) 值。
考慮弱化版 CF1045J 的做法,\(s_k\) 出現的位置要麼完全包含在 \(x\rightsquigarrow \text{lca}(x,y)\) 和 \(\text{lca}(x,y)\rightsquigarrow y\) 兩條直鏈內,要麼跨過了 \(\text{lca}(x,y)\)。
\(\bf{Case\space1}\):完全包含在直鏈內的情況
在這部分中考慮使用雜湊實現字串匹配。我們的雜湊方式為多項式雜湊,即對於字串 \(s\),其雜湊值為:
其中 \(\text{base}\) 為乘數,\(\text{MOD}\) 為模數。本題卡乘數和模數,我使用了【】生日的日期做乘數,\(10^9+9\) 做模數。不能自然溢位,因為後面需要用到逆元。
在弱化版中,我們運用 \(|S|\le 100\) 的條件,對於每一種長度的字串單獨處理。在此題中我們也可以如法炮製,需要運用到一個引理:
\(\bf{Lemma\space 1}\)
在字串總長度為 \(n\) 的長為 \(m\) 的字串序列 \(s_1\sim s_m\) 中,本質不同的字串長度種數為 \(\mathcal{O}(\sqrt{n})\) 級別。
\(\bf{Proof\space 1}\)
考慮 \(s_1\sim s_m\) 種出現了 \(k\) 種本質不同的長度,從小到大依次是 \(\text{len}_1\sim \text{len}_k\),記 \(\text{cnt}_i\) 表示第 \(i\) 種長度的出現次數,形式化地,\(\text{cnt}_i=\sum\limits_{j=1}^m[|s_j|=\text{len}_i]\)。
那麼有:\(\sum\limits_{i=1}^k(\text{len}_i\cdot \text{cnt}_i)=n\)。
可以發現 \(\text{len}_i\ge i,\text{cnt}_i\ge 1\)。後面那個很顯然,因為這種長度出現時一定存在一個字串滿足其長度為 \(\text{len}_i\)。
至於前面那個使用歸納法證明:
當 \(i=1\) 時 \(\text{len}_1\ge 1\) 顯然成立。
假設對於 \(i\in[1,p]\cup\mathbb{Z}\) 時成立,則對於 \(i\in[1,p+1]\cup\mathbb{Z}\) 時,由於 \(\text{len}_1\sim \text{len}_k\) 中的每一個數都是一種本質不同的長度,且從小到大排列,所以 \(\text{len}_{p+1}>\text{len}_p\ge p\)。由於都是整數,所以 \(\text{len}_{p+1}\ge p+1\)。
由於這裡涉及到的量都是正的,所以 \(\text{len}_i\cdot \text{cnt}_i\ge i\),因此 \(\sum\limits_{i=1}^ki\le \sum\limits_{i=1}^k(\text{len}_i\cdot \text{cnt}_i)=n\),因此有 \(\dfrac{k^2+k}{2}\le n\)。可以得到 \(k\le \sqrt{2n}=\mathcal{O}(\sqrt{n})\)。注意這裡不是在解不等式,由 \(\dfrac{k^2+k}{2}\le n\) 推匯出一個成立的條件。
證畢。
那麼我們可以對於這 \(\mathcal{O}(\sqrt{n})\) 種長度分別求解。在求解一種長度 \(\text{len}\) 的詢問時,我們對於每個點 \(u\) 預處理 \(\text{str}(u,\text{anc}(\text{len},u))\) 和 \(\text{str}(\text{anc}(\text{len},u),u)\) 的雜湊值 \(\text{uk}_u\) 和 \(\text{dk}_u\)(若不存在則不處理)。需要先預處理 \(\text{up}_u\) 和 \(\text{dwn}_u\) 表示 \(\text{str}(u,1)\) 和 \(\text{str}(1,u)\) 的雜湊值。容易得到:
-
\(\text{up}_u\equiv \text{up}_{\text{fa}_u}+\text{val}_u\cdot \text{base}^{\text{dep}_u-1}\pmod{ \text{MOD}}\)
-
\(\text{dwn}_u\equiv\text{dwn}_{\text{fa}_u}\cdot \text{base}+\text{val}_u\pmod{ \text{MOD}}\)
由於這個做法比較垃圾,我們不能在求解每種長度時重新遍歷樹計算雜湊值,否則會超時。可以考慮犧牲空間,在第一次遍歷樹時就對於每個點存下這些雜湊值。這樣可以省去 \(\mathcal{O}(\sqrt{n})\) 次遍歷樹的時間。
\(\text{uk}_u\) 和 \(\text{dk}_u\) 都可以透過與 \(\text{anc}(\text{len},u)\) 的雜湊值差分得到,注意差分時的移位操作。
具體地:
-
\(\text{uk}_u\equiv \dfrac{\text{up}_u-\text{up}_{\text{anc}(\text{len},u)}}{\text{base}^{\text{dep}_u-\text{len}}}\pmod{\text{MOD}}\)
-
\(\text{dk}_u\equiv \text{dwn}_u-\text{dwn}_{\text{anc}(\text{len},u)}\cdot \text{base}^{\text{len}}\pmod{\text{MOD}}\)
可以預處理 \(\text{base}\) 的若干次方以及對應的逆元。考慮怎麼快速求 \(\text{anc}(\text{len},u)\)。由於查詢次數為 \(\mathcal{O}(n\sqrt{n})\),所以單次查詢必須為 \(\mathcal{O}(1)\)。可以考慮維護 \(1\rightsquigarrow u\) 構成的序列 \(\text{stk}\),使得 \(\text{stk}_i\) 為路徑上的第 \(i\) 個點。則所求即為 \(\text{stk}_{\text{dep}_u-\text{len}}\)。每次遍歷到一個點 \(u\) 時,將 \(u\) 加入序列末尾。結束 \(u\) 的遍歷時,將 \(u\) 從序列末尾刪除。
在求解每種長度時再考慮對於每種詢問串的雜湊值 \(\text{hsh}\) 單獨求解。考慮記 \(\text{num}_{0,u}\) 表示 \(1\rightsquigarrow u\) 上有多少個點 \(v\) 滿足 \(\text{uk}_v=\text{hsh}\),\(\text{num}_{1,u}\) 表示 \(1\rightsquigarrow u\) 上有多少個點 \(v\) 滿足 \(\text{dk}_v=\text{hsh}\)。這個可以考慮從 \(1\) 到 \(n\) 掃描 \(i\),依次維護 \(v\in[1,i]\) 的情況,則每次新掃到一個位置,需要修改(\(+1\))的值拍平成 \(\text{dfn}\) 序後形如一段區間。
至於詢問,對於 \(x\rightsquigarrow \text{lca}(x,y)\) 那條鏈上的貢獻,考慮匹配的起點在哪個位置,容易發現鏈上的一個點 \(u\) 能夠匹配當前詢問串當且僅當 \(\text{dep}_u-\text{dep}_{\text{lca}(x,y)}\ge \text{len}\),且 \(\text{uk}_u=\text{hsh}\)。因為這樣才能包含在直鏈內。進一步發現滿足這個條件的點位於 \(x\rightsquigarrow \text{anc}(\text{dep}_x-\text{len}-\text{dep}_{\text{lca}(x,y)},x)\) 上,那麼拿 \(\text{num}_{0,x}\) 和 \(\text{num}_{0,\text{anc}(\text{dep}_x-\text{len}-\text{dep}_{\text{lca}(x,y)},x)}\) 差分即可。\(\text{lca}(x,y)\rightsquigarrow y\) 的查詢方式類似,注意此時匹配的方向是自上而下,用 \(\text{num}_1\) 值差分計算。
此時,一共有 \(\mathcal{O}(n\sqrt{n})\) 次修改,\(\mathcal{O}(n)\) 次查詢,維護以 \(\text{dfn}\) 序為下標的差分陣列,那麼只需要分塊支援 \(\mathcal{O}(1)\) 單點修改,\(\mathcal{O}(\sqrt{n})\) 字首查詢即可。
值得注意的是,為了將同種雜湊值的詢問一起做,考慮使用排序將它們排在一個連續的區間內時,需要使用基數排序確保排序複雜度線性,才能保證 \(\boldsymbol{\mathcal{O}(\sqrt{n})}\) 次排序的總複雜度為 \(\boldsymbol{\mathcal{O}(n\sqrt{n})}\)。
\(\bf {Case\space 2}\):跨過直鏈的情況
考慮分別處理每種串 \(s_k\) 的詢問。
假設跨過直鏈的匹配發生在 \(u\rightsquigarrow v\) 上,其中 \(u,v\) 是 \(x\rightsquigarrow y\) 上的節點且 \(u\) 在 \(v\) 前。此時一定滿足,\(\text{str}(u,\text{lca}(x,y))\) 是 \(s_k\) 的字首,\(\text{str}(\text{lca}(x,y),v)\) 是 \(s_k\) 的字尾。
同時,\(\text{str}(u,\text{lca}(x,y))\) 是 \(\text{str}(x,\text{lca}(x,y))\) 的字尾,\(\text{str}(\text{lca}(x,y),v)\) 是 \(\text{str}(\text{lca}(x,y),y)\) 的字首。
考慮找到最長的長度 \(P,Q\),使得 \(\text{str}(x,\text{lca}(x,y))\) 存在長度為 \(P\) 的字尾為 \(s_k\) 的字首;\(\text{str}(\text{lca}(x,y),y)\) 存在長度為 \(Q\) 的字首為 \(s_k\) 的字尾。
考慮一個基礎問題:
給出字串 \(a,b\),找到 \(b\) 的最長字首使得它是 \(a\) 的字尾。求出這個最長長度。
解決方法是:找到 \(a\) 的一個字尾 \(a[i,|a|]\) 使得 \(|\text{lcp}(a[i,|a|],b)|\) 最大。記 \(L=|\text{lcp}(a[i,|a|],b)|\),則 \(a[i,|a|]\) 最長的長度不超過 \(L\) 的 \(\text{border}\) 的長度即為所求。
接下來證明正確性。
\(\bf {Proof\space 2}\)
首先,這個 \(\text{border}\) 一定是同時是 \(b\) 的字首和 \(a\) 的字尾。因為它是 \(a[i,|a|]\) 的字首又是它的 \(\text{border}\),說明 \(a[i,|a|]\) 存在這個 \(\text{border}\) 作為字尾。自然 \(a\) 也存在這個 \(\text{border}\) 作為字尾。記這裡求出來的長度為 \(\text{len}\)。
考慮是否存在更長的答案。假設存在更長的答案長度為 \(\text{tmp}\),其一定不超過 \(L\),不然 \(a[i,|a|]\) 就不是使得 \(\text{lcp}\) 長度更大的字尾了。此時,\(a[i,|a|]\) 與 \(b\) 存在長度為 \(\text{tmp}\) 的 \(\text{lcp}\)。這時候 \(a[i,|a|]\) 開頭的 \(\text{tmp}\) 個字元形成的字串與結尾的 \(\text{tmp}\) 個字元形成的字串相等。此時 \(\text{tmp}\) 是 \(a[i,|a|]\) 的一個更長的、長度不超過 \(L\) 的 \(\text{border}\),矛盾。
因此不存在更長的答案,\(\text{len}\) 即為所求。
證畢。
將原問題轉化成上述形式,那麼 \(P\) 就是 \(\text{str}(\text{lca}(x,y),x)\) 最長的字首長度滿足它是 \(s_k^R\) 的字尾。這個和原問題顯然是等價的,因為兩個詢問串都是原問題詢問串的反串。不妨令新問題答案為 \(w\),則反過來後原串對應的位置也相等,原問題的答案至少為 \(w\);若原問題存在更長的答案 \(z\),則反串中這些對應的位置也相等,\(z\) 就是新問題的一個更長的答案,與 \(w\) 的定義矛盾。
因此,\(P\) 就是在這個基礎問題中 \(b=\text{str}(\text{lca}(x,y),x),a=s_k^R\) 的情況;\(Q\) 就是在這個基礎問題中 \(b=\text{str}(\text{lca}(x,y),y),a=s_k\) 的情況。
先對 \(s_k\) 以及 \(s_k^R\) 進行字尾排序。
最長的長度不超過 \(L\) 的 \(\text{border}\) 很好求,由於要求的是某個字尾的 \(\text{border}\),在其反串上就變成了字首的 \(\text{border}\),這兩個問題也是等價的,和上面的證明類似。
因此,對於 \(s_k\) 和 \(s_k^R\) 建立失配樹 \(T_k\) 和 \(T_k^R\),並進行輕重鏈剖分。找到滿足條件的字尾後,先看一下它的反串對應的是哪一棵失配樹,然後在失配樹上一條一條重鏈向上跳。失配樹的根鏈是單調遞減的(從節點到根)。若鏈頂大於 \(L\),就整條鏈跳過,否則在鏈上二分,單次詢問時間複雜度為 \(\mathcal{O}(\log n)\)。
接著考慮如何找到使得 \(L\) 最大的字尾。這個字尾一定滿足,它要麼是 \(a\) 中最大的字典序大小不超過 \(b\) 的字尾,要麼是 \(a\) 中最小的字典序大小個超過 \(b\) 的字尾。換句話說,設這兩個字尾與 \(b\) 的 \(\text{lcp}\) 長度分別為 \(A,B\),則 \(L=\max\{A,B\}\)。
接下來給出證明:
\(\bf{Proof\space 3}\)
設它們的排名分別為 \(i,j\)。則一定有 \(j=i+1\)。因為根據定義,排名為 \(i+1\) 的字尾字典序大小已經超過了 \(b\),但是排名在 \([1,i]\cup\mathbb{Z}\) 內的字尾字典序大小都不超過 \(b\)。
考慮反證,假設排名為 \(\text{rnk}(\text{rnk}\ne i\land \text{rnk}\ne i+1)\) 的字尾會得到更大的 \(\text{lcp}\) 長度。
記這個更大的 \(\text{lcp}\) 長度為 \(\text{len}\)。分兩種情況討論:
若 \(\text{rnk}\in[1,i)\cup\mathbb{Z}\),則排名為 \(\text{rnk}\) 的字尾的前 \(\text{len}\) 位均與 \(b\) 的前 \(A\) 位相同。根據 \(\text{len}\) 的定義可知其第 \(A+1\) 位也與 \(b\) 的這一位相同。根據定義,排名為 \(i\) 的字尾的第 \(A+1\) 位小於 \(b\) 的這一位,或者說這一位不存在(空字元)。此時,排名為 \(i,\text{rk}\) 的兩個字尾前 \(A\) 位相同都等於 \(b\) 的前 \(A\) 位。且後者的第 \(A+1\) 位大於前者的這一位。說明後者比前者字典序大,這與 \(\text{rnk}\in[1,i)\cup\mathbb{Z}\) 矛盾。
若 \(\text{rnk}\in(i+1,|a|]\cup \mathbb{Z}\),與上一種情況類似推導得到字典序大小上的矛盾即可證明。
證畢。
於是考慮求得排名 \(i\)。由於經過字尾排序,即這些字尾的字典序遞增,所以答案有單調性,直接二分這個排名即可。
考慮如何求一條鏈上的字串和序列上的字串的最長公共字首長度。對原樹進行輕重鏈剖分,將邊權轉化為深度較深的端點的點權,則這條鏈會被表示成 \(\mathcal{O}(\log n)\) 條連續的重鏈區間。對於 \(\text{dfn}\) 序列形成的字串維護雜湊,對 \(s_k\) 和 \(s_k^R\) 也維護雜湊。
一條一條重鏈匹配,若能全部匹配上,就算上這些長度,否則二分第一個不同的位置。只有第一條不匹配的重鏈需要二分,因此時間複雜度為 \(\mathcal{O}(\log n)\)。
這部分細節比較多,尤其是一方匹配完的邊界情況,具體看程式碼中的 qlcp
部分。
此時,這個過程已經求出了 \(\text{lcp}\) 長度,順帶比較一下大小配合套在外面的二分。
那麼 \(P,Q\) 均被我們求出來了。
求出來之後,我們只需要考慮 \(x\rightsquigarrow \text{lca}(x,y)\) 的後 \(P\) 個位置為開頭處形成的匹配,因為不能匹配 \(s_k\) 更長的字首了。
記這 \(P\) 個位置依次為 \(u_1\sim u_P\),滿足它們按照 \(\text{lca}(x,y)\rightsquigarrow x\) 路徑上的順序排列;記後 \(Q\) 個位置依次為 \(v_1\sim v_Q\),滿足它們按照 \(\text{lca}(x,y)\rightsquigarrow y\) 路徑上的順序排列。
則 \(u_i\) 為開頭處能形成合法的匹配,當且僅當一下三點同時滿足:
- \(s_k[1,P]\) 存在長度為 \(i\) 的 \(\text{border}\)。
- \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\) 的 \(\text{border}\)。
- \(i\ne |s_k|\)。
證明:
\(\bf{Proof \space 4}\)
充分性:
因為 \(i\ne |s_k|\),所以跨過了 \(\text{lca}(x,y)\)。因為 \(s_k[1,P]\) 存在長度為 \(i\) 的 \(\text{border}\),根據 \(P\) 的定義可以得到 \(\text{str}(u_i,\text{lca}(x,y))=s_k[P-i+1,P]=s_k[1,i]\);因為 \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\) 的 \(\text{border}\),類似地,\(\text{str}(v_{|s_k|-i},\text{lca}(x,y))=s_k^R[1,|s_k|-i]\),根據反串的定義得到 \(\text{str}(\text{lca}(x,y),v_{|s_k|-1})=s_k[i+1,|s_k|]\)。兩者拼接恰好是 \(s_k\)。
必要性:
若 \(u_i\) 開頭處可以形成合法的匹配,首先一定有 \(i\ne |s_k|\),因為要跨過 \(\text{lca}(x,y)\)。其次 \(\text{str}(u_i,\text{lca}(x,y))=s_k[1,i]\),根據 \(P\) 的定義,\(\text{str}(u_i,\text{lca}(x,y))=s_k[P-i+1,P]\),因此 \(s_k[1,i]=s_k[P-i+1,P]\),即 \(s_k[1,P]\) 存在長度為 \(i\) 的 \(\text{border}\);類似地,\(\text{str}(v_{|s_k|-i},\text{lca}(x,y))=s_k^R[1,|s_k|-i]=s_k^R[Q-|s_k|+i+1,Q]\),因此 \(s_k^R[1,Q]\) 存在長度為 \(|s_k|-i\) 的 \(\text{border}\)。
證畢。
所以,我們要統計有多少 \(i\) 合法,就是要統計有多少 \(i\) 滿足這三個條件。
轉化成失配樹上的限制,就是要求有多少 \(i\) 滿足 \(i\) 在 \(T_k\) 中 \(P\) 的根鏈上,且 \(|s_k|-i\) 在 \(T^R_k\) 中 \(Q\) 的根鏈上。
考慮離線 + 掃描線。對於所有 \(s_k\) 的詢問,將它掛在 \(T_k\) 的 \(P\) 節點上。考慮深度優先搜尋 \(T_k\),在過程中一併維護陣列 \(a_0\sim a_{|s_k|}\)。其中 \(a_j\) 表示有多少 \(i\),滿足:
- \(i\) 在 \(T_k\) 的當前搜到的點 \(u\) 的根鏈上。
- \(|s_k|-i\) 在 \(T_k^R\) 中點 \(j\) 的根鏈上。
- \(i\ne |s_k|\)。
則只要在 \(P\) 處單點查 \(a_Q\) 即可。
每次新掃到一個點 \(u\),則和上一層深搜相比根鏈上增加了一個點 \(u\) 的貢獻。考慮 \(|s_k|-u\) 的貢獻,此時首先滿足 \(u\ne |s_k|\),發現只有它子樹內的點的根鏈經過它,即只要這些點的 \(a_j\) 值要增加 \(u\) 的貢獻。拍平成 \(\text{dfn}\) 序後將 \(a\) 對映過去,再進行差分,則需要支援的操作形如區間加、單點查,由於此處修改、詢問同階,樹狀陣列維護即可。每次結束一個點的深搜時,刪去它的貢獻。
這部分就做完了,時間複雜度為 \(\mathcal{O}\left(n\log^2 n\right)\)。
綜上,這個做法時空複雜度均為 \(\mathcal{O}(n\sqrt{n})\),可以接受。前面也說過空間可以做到線性,只是需要一些精湛的卡常技藝。
AC Link & Code
碼量 12.5 KB。
總結一下,本題考察了一下幾方面的知識:
- 思想:離線、掃描線、平衡複雜度等。
- 樹論:輕重鏈剖分、最近公共祖先等。
- 資料結構:樹狀陣列、分塊等。
- 字串:字尾陣列、失配樹、字串雜湊等。
- 數論:快速冪、乘法逆元等。
題目思路巧妙、性質極多、碼量極大,對於選手的思維能力和程式碼能力都是一種不小的挑戰,算是一道不可多得的字串神仙 & 毒瘤題目。
完結撒花!