LA_3942 LA_4670 從字典樹到AC自動機
首先看第一題,一道DP+字典樹的題目,具體中文題意和題解見訓練指南209頁。
初看這題模型還很難想,看過藍書提示之後發現,這實際上是一個標準DP題目:通過數組來儲存後綴節點的出現次數。也就是用一顆字典樹從後往前搜一發。最開始覺得這種搞法怕不是要炸時間,當時算成了O(N*N)畢竟1e5的數據不搞直接上N*N的大暴力。。。後來發現,字典樹根本跑不完N因為題目限制字典樹最多右100層左右。
實際上這道題舊思想和模型來說很好(因為直觀地想半天還真想不出來。。)但是實際實現起來很簡單——擼一發字典樹就好了。然而專門寫一篇博客是因為自從學了劉汝佳的字典樹之後就發現之前自己寫的那個實在是太不優雅(使用了大量指針,還牽扯到內存回收的鬼故事),反而不如劉汝佳這種,一個類搞定一切,方便快捷,也不會因為莫名的bug調試一下午什麽的。。於是來說說劉汝佳字典樹的實現方式:
- 一個二維數組,cha【MAXN】【SIGMA_SIZE】用來存子節點的位置
- 一個標記數組,val【MAXN】用來儲存每個節點的相關信息,比如是不是單詞的結尾、第幾次出現等
- 一個變量,size起到類似於棧頂指針的作用。
整體上,訓練指南的字典樹實現方案類似於一個大型棧,開開之後就一路往進壓元素就好了。因而插入節點的時候很容易聯想到入棧的過程。同時,整個字典樹初始化時的常數也很小——不需要回收整棵字典樹,只需要講字典樹的根節點指針置零、棧指針size置一就好;在每次增加元素的時候也只需要把當前元素的指針提前置零即可。
下面放AC代碼:
#include<bits/stdc++.h> usingnamespace std; const long long MAXN=300233; char str[MAXN]; long long len=0; long long dp[MAXN]; const long long MOD=20071027; class AC_AUTO { public: long long cha[MAXN][26]; long long f[MAXN]; long long last[MAXN]; long long val[MAXN]; long long size; AC_AUTO() { init(); }void init() { memset(cha[0],0,sizeof(cha[0])); //避免大規模初始化浪費時間 size=1; // memset(val,0,sizeof(val)); } void insert(char *tar) { int len=strlen(tar); int u=0; for(int i=0;i<len;++i) { if(!cha[u][tar[i]-‘a‘]) { memset(cha[size],0,sizeof(cha[size])); val[size]=0; cha[u][tar[i]-‘a‘]=size; size++; } u=cha[u][tar[i]-‘a‘]; }val[u]=1; } bool find(char *tar) { int l=strlen(tar); int u=0;int p1=len-l; for(int i=0;i<l;++i) { if(!cha[u][tar[i]-‘a‘])return false; u=cha[u][tar[i]-‘a‘]; if(val[u]) { dp[p1]+=dp[p1+i+1]; dp[p1]%=MOD; } }return val[u]; } };AC_AUTO t1; long long kk=1; void init() { memset(dp,0,sizeof(dp)); t1.init(); len=strlen(str); long long n; cin>>n; for(int i=0;i<n;++i) { char sub[233]; cin>>sub; t1.insert(sub); } dp[len]=1; for(int i=len-1;i>=0;--i) { t1.find(str+i); } cout<<"Case "<<kk++<<": "<<dp[0]<<"\n"; } int main() { cin.sync_with_stdio(false); while(cin>>str)init(); return 0; }
事實上我寫第一題主要是為了在第一題的基礎上實現後面劉汝佳規約的AC自動機,於是上面代碼的類名依然是AC_AUTO。劉汝佳規約的AC自動機首先是一顆字典樹——加了失配邊和後綴指針的字典樹。
因而在上述字典樹的基礎上應當加入:
- f【MAXN】表示適配函數
- last【MAXN】表示失配函數中的最近一個單詞節點(VAL【】不為零)
AC自動機在功能上應當是一個多重KMP,因而從原理上認為實現方式上應當等同於KMP——按照出現順序向後遍歷並在該過程中不斷尋找失配邊。於是考慮字典樹情況,也應當按照層數逐漸遞增的形式進行匹配,因而認為BFS很合適實現這個算法——(實現樹的層次遍歷),於是建立失配邊的過程類似基本類似於KMP+BFS
本體有些坑在於數組尺寸的調教,如果沒整好。。。就地TLE。。(不是數組越界是T。。)
另外訓練指南中推薦使用map來保存字符串的出現順序以避免重復情況,但是考慮到map直接使用【】來進行操作有比較大的常數,考慮到本身AC自動機就是一個字典樹,於是強行在字典樹中查詢可能結果會更好。
然而。。。做了這個優化之後並沒有發現實質的效率提升。。都是46毫秒。。。
#include<bits/stdc++.h> using namespace std; const long long MAXN=70*26+23003; const long long SIGMA_SIZE=30; char str[1000233]; char input[233][100]; long long cnt[233]; long long len=0,n=0; const long long MOD=20071027; map<string,int> ms; //char anss[1000233]; class AC_AUTO { public: long long cha[MAXN][SIGMA_SIZE]; long long f[MAXN]; long long last[MAXN]; long long val[MAXN]; long long size; AC_AUTO() { init(); } void init() { memset(cha[0],0,sizeof(cha[0])); //避免大規模初始化浪費時間 size=1; // memset(val,0,sizeof(val)); } void insert(char *tar,int numb) { int len=strlen(tar); int u=0; for(int i=0;i<len;++i) { if(!cha[u][tar[i]-‘a‘]) { memset(cha[size],0,sizeof(cha[size])); val[size]=0; cha[u][tar[i]-‘a‘]=size; size++; } u=cha[u][tar[i]-‘a‘]; }val[u]=numb;//ms[string(tar)]=numb; } void print(int j) { if(j) { cnt[val[j]]++; print(last[j]); } } void find(char *tar) { int n=strlen(tar); int j=0; for(int i=0;i<n;++i) { int c=tar[i]-‘a‘; while(j&& !cha[j][c])j=f[j]; j=cha[j][c]; if(val[j])print(j); else if(last[j])print(last[j]); } } void getfail() { queue<int> q; f[0]=0; for(int c=0;c<SIGMA_SIZE;++c) { int u=cha[0][c]; if(u) { f[u]=0;q.push(u); last[u]=0; } } while(!q.empty()) { int r=q.front();q.pop(); for(int c=0;c<SIGMA_SIZE;++c) { int u=cha[r][c]; if(!u)continue; q.push(u); int v=f[r]; while(v&&!cha[v][c])v=f[v]; f[u]=cha[v][c]; last[u]= val[f[u]]? f[u]:last[f[u]]; } } } long long get(char *tar ) { int l=strlen(tar ); int u=0; for(int i=0;i<l;++i) { u=cha[u][tar[i]-‘a‘]; } return val[u]; } };AC_AUTO a1; void init() { memset(cnt,0,sizeof(cnt)); // ms.clear(); a1.init(); for(int i=1;i<=n;++i) { scanf("%s",input[i]); a1.insert(input[i],i); } a1.getfail(); scanf("%s",str); a1.find(str); long long ans=-1; for(int i=0;i<=n;++i) { if(cnt[i]>ans)ans=cnt[i]; } printf("%lld\n",ans); for(int i=1;i<=n;++i) { if(cnt[a1.get(input[i])]==ans)printf("%s\n",input[i]); // else cout<<"not "<<input[i]<<ends<<cnt[ms[string(input[i])]]<<endl; } } int main() { // cin.sync_with_stdio(false); while(scanf("%lld",&n)==1&&n)init(); return 0; }
LA_3942 LA_4670 從字典樹到AC自動機