AC自動機題目選講
AC自動機題目選講
AC自動機複習:AC 自動機 - OI Wiki
先完成模板:luogu的
下面的所有例題程式碼我用的是ldytxdy這個賬號提交,可以直接在cf上檢視程式碼。
複習題
\(1.\)CF1202E
列舉斷點,設 \(\large s_i\) 和 \(\large rs_i\) 分別表示以\(i\)為結尾的字首/字尾的匹配個數,答案為
\[\large \sum_{i=2}^{n}s_i \times rs_{i-1} \]\(\large s_i\) 和 \(\large rs_i\) 可以通過分別建正反串AC自動機求出。
AC自動機上dp
\(2\).CF696D
設 \(\large f_{i,j}\) 為答案串匹配到前\(i\)位,在AC自動機的 \(\large j\) 節點的最大值
\(\large val(x)\) 表示把原串權值記在\(x\)號點
\[\large f_{0,0} = 1\\ \large f_{i,j} + val(tr_{j,k})\to f_{i+1,tr_{j,k}} \]矩乘優化即可。
\(3\).SDOI2014 數數
設 \(\large \ f_{pos,p}\) 表示答案數位在 \(\large pos\) 且AC自動機節點在 \(\large p\) 的方案數
設 \(\large op(x)\)
用數位dp實現這個過程
練一練:CF86C
簡單資料結構+AC自動機
\(5.\)NOI2011 阿狸的打字機
考慮題目中加減字元的實際意義就是在trie樹上dfs,所以我們把詢問離線到每個結束點上做一遍dfs
我們建出fail樹,考慮AC自動機跳fail樹的匹配過程,需要實現的操作就是單點加和查子樹和
記下fail樹的dfn序後樹狀陣列,記得dfs回溯的時候撤銷貢獻。
\(6\).CF547E
考慮離線,每個詢問可以拆成字首和相減的形式,用vector記錄每個位置的詢問
每掃到一個字串,在fail樹上把這個字串代表的所有節點權值 \(\large +1\)
答案就是查 \(\large s_k\) 在fail樹上結束點的子樹和。套用上題做法即可。
\(7.\)CF1437G
根據上兩題的經驗很容易做出來,需要實現單點加和點到根的總權值,樹剖維護。
練一練:CF163E
好題
\(8\).CF710F
CF163E的線上版本
AC自動機的刪除是很困難的
建出兩個AC自動機,分別塞入刪除和增加的串,答案就是在兩個AC自動機上分別做一遍匹配然後相減。
考慮二進位制分組,每組建一個AC自動機,加入時往前合併,具體實現與線段樹合併類似。
顯然每個串至多合併 \(\large log_2n\) 次,所以複雜度是 \(\large Slogn\) , \(\large S\) 表示總串長。
\(9.\)CF587F
因為我們做了CF547E,所以可以很容易看出答案就是:
將 \(\large l...r\) 中所有\(\large s_i\)的結束點子樹每個點 \(\large +1\) 後 \(\large s_k\) 代表字元所有節點的總權值。
這樣就不能像之前的題用樹狀陣列的單點加實現了,考慮離線根號分治。
設 \(\large len_i\) 表示 \(\large s_i\) 的長度,設所有 \(\large s_i\) 總長度為 \(\large M\) ,並且設一個閾值 \(\large L\) .
\(\large I.\)\(\large len_i \leqslant L\)
依次掃過 \(n\) 個串,每掃到一個串用樹狀陣列實現子樹加,查的時候暴力單點查一個字串的所有點。
複雜度分析:子樹加是 $\large O(nlog_2M) $的,查詢是 \(\large O(QLlog_2M)\) 的
\(\large II.\)\(\large len_i>L\)
顯然這樣的 \(\large s_i\) 個數是不超過 \(\large \frac M L\) 的
還是依次掃過 \(\large n\) 個串,我們對於每個 \(\large s_i\) 的節點做一次單點加,查就是一個子樹和。
這個可以不用資料結構,暴力做就好了。
複雜度分析:單點加: \(\large O(\frac {M^2} L)\) ,因為要把詢問排序所以詢問的複雜度是 \(\large O(Qlog_2Q)\) 的
利用均值不等式設 \(\large \frac {M^2} L = QLlog_2n\) 解得 \(\large L=\frac {m} {\sqrt {qlog_2m}}\)
所以總複雜度是 \(\large O(nlog_2M+Qlog_2Q+M\sqrt {Qlog_2M})\)
\(10.\)CF1483F
顯然 \(\large (i,j)\)合法當且 \(\large s_j\) 在 \(\large s_i\) 中不被任意一個 \(\large s_k\) 覆蓋
我們列舉每個字串作為 \(\large i\),從後往前列舉 \(\large s_i\)的每個位置 \(\large p\)
我們需要尋找出以 \(\large p\) 結尾的最長子串 \(\large S\) 使得 \(\large S \in s_{1...n}\),設 \(\large S=s_x\)
設 \(\large S\) 的左端點為 \(\large L\),設 \(\large pre\) 為之前掃過的最大的 \(\large L\)
如果 \(\large pre>L\) 那麼這就是 \(\large s_x\) 不被包含的一次,此時 \(\large cnt_{s_x}+1\),並用 \(\large L\) 更新 \(\large pre\)
最後答案加上有多少個 \(\large cnt_i\) 滿足 \(\large cnt_i=\) 總出現個數
經典工業題
模板
最後附上我的板子,是這個的AC程式碼。
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n;char s[N];
int tr[N][27],fail[N],End[N],tot;
queue<int>q;
void ins(char *s){
int p=0,m=strlen(s);
for(int i=0;i<m;i++){
int l=s[i]-'a';
if(!tr[p][l])
tr[p][l]=++tot;
p=tr[p][l];
}
End[p]++;
}
void build(){
for(int i=0;i<26;i++)
if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u=q.front();q.pop();
for(int i=0;i<26;i++){
if(tr[u][i]){
fail[tr[u][i]]=tr[fail[u]][i];
q.push(tr[u][i]);
}
else tr[u][i]=tr[fail[u]][i];
}
}
}
int query(char *s){
int m=strlen(s),p=0,ans=0;
for(int i=0;i<m;i++){
p=tr[p][s[i]-'a'];
for(int t=p;t&&~End[p];t=fail[t])
ans+=End[t],End[t]=-1;
}return ans;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s);
ins(s);
}
build();
scanf("%s",s);
return printf("%d\n",query(s))&0;
}