P5357【模板】AC自動機(二次加強版)
阿新 • • 發佈:2022-05-13
一、題解分析
因為是什麼二次加強版,所以大家先去做一下加強版吧,做法差不多。
沒做過的看上面的連結,有一些變數名可能會在剛才那一篇\(blog\)出現過,所以建議大家再去過一下。
好了,看到這裡大家都一定做過加強版了吧,那麼這道題的做法也是差不多的:我們這一次不需要求出現最多的字串啦,直接將\(cnt\)陣列輸出就好了!(應該都知道\(cnt\)陣列是什麼吧,就是統計每個模式串在文字串出現多少次的陣列)
但重複的單詞有沒有影響啊!有啊!對於加強版,這一次重複的單詞就會有影響啦,怎麼辦?
這道題有相同字串要統計,設當前字串是第\(i\)個,我們用一個\(Map[i]\)陣列(不是\(STL\)
二、樸素版本
//本程式碼手動吸氧,可以得76分 #include <bits/stdc++.h> using namespace std; const int N = 2 * 1e5 + 10; const int M = 2 * 1e6 + 10; //最大長度 2*10^6,比加強版大了一倍 char s[N], T[M]; //模式串與文字串 /* 每個模式串Si在T中出現的次數 資料不保證任意兩個模式串不相同。 理解: 1、如果能保證模式串不重複,那麼就是加強版。 2、由於模式串可能重複,所以需要進行一下處理:採用一個類似於並查集的思想: 因為模式串可能有重複的,而我們在本題中不關心第幾個模式串出現幾次,只關心“長著一樣的字串,出現了幾次” 比如:第2、3兩個序號輸入的模式串都是 abcd,其實,我們關注的是 abcd出現的次數,而不是 2和3出現的次數。 因為,採用的辦法就是:一樣的字串,第一次錄入記錄它是family的族長,後面再輸入一樣的字串,都使用前面的族長編號做為自己家族的標識 */ int n; //模式串數量 int tr[N][26], idx; // Trie樹 int id[N]; // 節點號-mapping->模式串 int ne[N]; // AC自動機的失配陣列 fail指標 int ans[N]; //記錄答案,每個模式串被命中幾次 int family[N]; //每個輸入的模式串x,都隸屬於某一個family[x],黃海稱之為家庭,類似於並查集? void insert(char *s, int x) { int p = 0; for (int i = 0; s[i]; i++) { int t = s[i] - 'a'; if (!tr[p][t]) tr[p][t] = ++idx; p = tr[p][t]; } if (!id[p]) id[p] = x; //當字串重複時,保留第一個錄入的序號 family[x] = id[p]; // 記錄族長資訊 } int q[N]; void bfs() { int hh = 0, tt = -1; //將佇列的頭和尾變數寫在這裡,可以有效防止多組測試資料的初始化問題 for (int i = 0; i < 26; i++) if (tr[0][i]) q[++tt] = tr[0][i]; while (hh <= tt) { int p = q[hh++]; for (int i = 0; i < 26; i++) { int t = tr[p][i]; // p狀態,通過i這條邊,到達的新狀態t; 也可以理解為是字首 if (!t) tr[p][i] = tr[ne[p]][i]; //節點 指向父節點失配指標的i這條邊 else { ne[t] = tr[ne[p]][i]; //失配指標指向父節點失配指標的i這條邊 q[++tt] = t; //存在的要入佇列 } } } } //查詢字串s在AC自動機中出現的次數 void query(char *s) { int p = 0; //從root出發 for (int i = 0; s[i]; i++) { //列舉文字串每個字元 int j = s[i] - 'a'; // j為當前字元的對映數字 //每走到一個位置,需要進行檢查 int k = tr[p][j]; // k為拷貝出來的臨時變數,準備遊標去一路向上尋找 while (k) { //如果還沒有到達root節點 if (id[k]) ans[id[k]]++; //如果k這個節點是某個模式串的終點,此模式串號 id[k] 對應的命中個數+1 k = ne[k]; //繼續向上遊動 } p = tr[p][j]; //走Trie樹路徑 } } int main() { scanf("%d", &n); //由模式串構建Trie樹 for (int i = 1; i <= n; i++) { scanf("%s", s); insert(s, i); } //構建AC自動機 bfs(); //輸入文字串 scanf("%s", T); //查詢 query(T); //對於每一個模式串,輸出它代表的字串在文字串中出現的次數 for (int i = 1; i <= n; i++) printf("%d\n", ans[family[i]]); return 0; }
三、拓撲序優化遞推版本
#include <bits/stdc++.h> using namespace std; const int N = 200010; //模式串長度 const int M = 2000010; //文字串長度 int n; //模式串個數 char s[N], T[M]; //模式串,文字串 // Trie樹 int tr[N][26], idx, id[N]; void insert(int x) { int p = 0; for (int i = 0, p = 0; s[i]; i++) { int t = s[i] - 'a'; if (!tr[p][t]) tr[p][t] = ++idx; p = tr[p][t]; } id[x] = p; } //構建AC自動機 int q[N], ne[N]; void bfs() { int hh = 0, tt = -1; for (int i = 0; i < 26; i++) if (tr[0][i]) q[++tt] = tr[0][i]; while (hh <= tt) { int t = q[hh++]; for (int i = 0; i < 26; ++i) { if (tr[t][i]) { ne[tr[t][i]] = tr[ne[t]][i]; q[++tt] = tr[t][i]; } else tr[t][i] = tr[ne[t]][i]; } } } int f[N]; void query(char *s) { int p = 0; for (int i = 0; s[i]; i++) { //列舉文字串每一個字元 int t = s[i] - 'a'; //字元對映的數字t,可以理解為邊 p = tr[p][t]; //走進去,到達的位置替換p f[p]++; //標識此位置有人走過,記錄走的次數 } // Trie樹中的節點個數=root(1個)+有效字元數(idx) //從大到小列舉,是因為這是符合拓撲序的,倒著來可以形成遞推的效果 for (int i = idx; i; i--) f[ne[q[i]]] += f[q[i]]; //一路向上,計算疊加值 } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%s", s); insert(i); } //構建AC自動機 bfs(); //文字串 scanf("%s", T); query(T); //輸出 for (int i = 1; i <= n; i++) printf("%d\n", f[id[i]]); return 0; }