「筆記」AC 自動機

寫在前面

這篇文章的主體是在沒網的悲慘情況下完成的。html

前置知識:Trie 樹DFAKMP 字符串匹配算法
請務必深入理解!node

定義

\(|\sum|\):字符集大小,在大多數題目中都等於小寫字母個數 26。git

\(s[i:j]\):字符串 \(s\) 的子串 \(s_i\cdots s_j\)
真前/後綴:字符串 \(s\) 的真前綴定義爲知足不等於它自己的 \(s\) 的前綴。同理就有了真後綴的定義:知足不等於它自己的 \(s\) 的後綴。github

\(\operatorname{border}\):字符串 \(s\)\(\operatorname{border}\) 定義爲,知足既是 \(s\) 的真前綴,又是 \(s\) 的真後綴的最長的字符串 \(t\)
\(\texttt{aabaa}\)\(\operatorname{border}\)\(\texttt{aa}\)算法

引入

P5357 【模板】AC自動機數組

給定一個文本串 \(s\)\(n\) 個模式串 \(t_1\sim t_n\),求在文本串中各模式串分別出現的次數。
字符串僅由小寫字母構成。可能出現重複的模式串。
\(1\le n\le 2\times 10^5\)\(\sum |t_i|\le 2\times 10^5\)\(1\le |s|\le 2\times 10^6\)函數

\(n = 1\),可使用 KMP 算法在 \(O(|s| + |t|)\) 的時空複雜度內求解。
AC 自動機能夠認爲是 KMP 算法在 Trie 樹上的應用,與 KMP 算法在失配時應用已匹配部分的 \(\operatorname{border}\) 進行跳轉相似,AC 自動機在失配時會根據失配指針跳轉到 Trie 樹上表明已匹配部分的 \(\operatorname{border}\) 的節點,從而加速匹配。post

值得注意的是,KMP 也是一種創建在模式串上的自動機。AC 自動機與 KMP 的關係,至關於 SAM 與 廣義 SAM 的關係。學習

構造

先把全部字符串插入 Trie 中。可能存在相同模式串,須要記錄每一個狀態表明的字符串的編號,可以使用 vector 實現。以後再考慮如何創建 ACAM。優化

void Insert(int id_, char *s_) {
  int now = 0, lth = strlen(s_ + 1);
  for (int i = 1; i <= lth; ++ i) {
    if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
    now = tr[now][s_[i] - 'a'];
  }
  id[now].push_back(id_); //記錄
}

暴力

按照 KMP 的思路直接構造。
與 KMP 相似地,記 \(\operatorname{fail}_u\) 表示從根節點到狀態 \(u\) 所表明的字符串(即已匹配部分)的 \(\operatorname{border}\) 對應的字符串的狀態。

在更新 \(\operatorname{fail}_u\) 前,必須保證比 \(u\) 深度淺的節點都已被更新過。則須要按照 bfs 的順序進行構造。
考慮使用 \(u\) 來更新 \(v=\operatorname{tr}(u,c)\) 的信息,其中 \(c\) 是 Trie 樹轉移邊上的字符,\(\operatorname{tr}(u,c)\) 表示在 \(u\) 按照轉移邊 \(c\) 轉移到的狀態。注意此處 \(\operatorname{tr}(u,c)\) 能夠不存在。
同 KMP,考察 \(\operatorname{tr}(\operatorname{fail}_u, c)\) 的存在性。若存在,則 \(\operatorname{fail}_{\operatorname{tr}(u,c)} = \operatorname{tr}({\operatorname{fail}_u, c})\)。若不存在則繼續考察 \(\operatorname{tr}(\operatorname{fail}_{\operatorname{fail}_u})\dots\),直到找到知足條件的狀態,或者到達根節點。

代碼以下:

void Build() {
  std::queue <int> q;
  for (int i = 0; i < 26; ++ i) {
    if (tr[0][i]) q.push(tr[0][i]);
  }
  while (! q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = 0; i < 26; ++ i) {
      int v_ = tr[u_][i], j = fail[u_];
      while (j && !tr[j][i]) j = fail[j]; //大力跳 fail
      if (tr[j][i]) j = tr[j][i]; //有出邊
      fail[v_] = j;
      if (v_) q.push(v_);
    }
  }
}

字典圖優化

能夠發現,在暴力的 while\(\operatorname{fail}\) 中,可能會出現重複的跳躍,這是暴力構建複雜度較高的主要緣由。
考慮將重複的跳躍進行路徑壓縮,能夠寫出以下的代碼:

void Build() {
  std::queue <int> q;
  for (int i = 0; i < 26; ++ i) {
    if (tr[0][i]) q.push(tr[0][i]);
  }
  while (! q.empty()) {
    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];
      }
    }
  }
}

稍微解釋一下。在暴力的代碼中,跳 \(\operatorname{fail}\) 是這樣的:while (j && !tr[j][i]) j = fail[j];
而在優化後的代碼中,\(\operatorname{fail}_u\) 已經指向了在未優化代碼中 \(j\) 最後的位置,所以能夠直接賦值 fail[tr[u_][i]] = tr[fail[u_]][i];。實現這一功能的關鍵是這一句:tr[u_][i] = tr[fail[u_]][i];

關於其原理,能夠考慮在暴力中什麼狀況下會屢次跳 \(\operatorname{fail}\)
顯然,當 while 中出現 \(\operatorname{tr}(\operatorname{fail}_u, i)\) 不存在的狀況時,纔會繼續考察 \(\operatorname{tr}(\operatorname{fail}_{\operatorname{fail}_u}, i)\) 的存在性。但在優化後,經過 tr[u_][i] = tr[fail[u_]][i]; 的賦值後,會讓本不存在的 \(\operatorname{tr}(\operatorname{fail}_u,i)\) 變爲 \(\operatorname{tr}(\operatorname{fail}_{\operatorname{fail}_u}, i)\),成爲一個「存在」的狀態。經過這種相似遞推的定義,從而完成了路徑壓縮的過程。

記 Trie 的節點個數爲 \(n\),優化後構建 ACAM 的時間複雜度顯然爲 \(O(n|\sum|)\)

匹配

在線

把文本串扔到 ACAM 上進行匹配。通過上述的路徑壓縮,若當前所在的狀態 \(u\) 不存在 \(s_i\) 的轉移,不須要大力跳 \(\operatorname{fail}\),能夠直接轉移到 \(tr(u:s_i)\)

設當前匹配到 \(s_i\),匹配到狀態 \(u\)。能夠發現,此時的已匹配部分(根到 \(u\) 的路徑)是 \(s[1,i]\) 的一段後綴,也是某模式串的一段前綴。

\(\operatorname{fail}\) 能夠認爲是在削除已匹配的前綴。在匹配過程當中,每跑到一個狀態,就暴力地跳 \(\operatorname{fail}\),便可枚舉出全部被已匹配部分包含的模式串的前綴
能夠在線地統計信息。

void Query(char *s_) {
  int now = 0, lth = strlen(s_ + 1);
  for (int i = 1; i <= lth; ++ i) {
    now = tr[now][s_[i] - 'a'];
    for (int j = now; j; j = fail[j]) { //枚舉已匹配部分包含的模式串
      for (int k = 0, lim = id[j].size(); k < lim; ++ k) { //累計答案
        sum[id[j][k]] ++;
      }
    }
  }
  for (int i = 1; i <= n; ++ i) printf("%d\n", sum[i]);
}

離線

能夠發現上述在線統計貢獻時只能每次令貢獻 \(+1\),算法複雜度上界顯然爲 \(O(n|t|)\)
P3808 【模板】AC 自動機(簡單版)P3796 【模板】AC自動機(增強版) 大多數人都採用了這種寫法。然而在 引入 中這種寫法會被卡到 60。

因而考慮離線操做,標記匹配狀態,再離線地統計貢獻。
對於引入中給出的問題,先把文本串 \(t\) 放到 ACAM 上跑一遍,記錄遍歷到了哪些狀態,並使改狀態出現次數 \(+1\)。枚舉到 \(t_i\) 時的狀態 \(now\) 表明了一個做爲 \(t[1:i]\) 的後綴最長某模式串的前綴
以後創建 \(\operatorname{fail}\) 樹,在 \(\operatorname{fail}\) 樹上 DP。根據 \(\operatorname{fail}\) 的定義和它們的相互包含關係,便可求得每一個狀態在文本串中出現的次數 \(\operatorname{size}\),從而獲得模式串的出現次數 \(\operatorname{sum}\)
上述作法相似樹上差分,記 Trie 的節點個數爲 \(n\),顯然總時間複雜度 \(O(|t| + n)\) 級別。

void Dfs(int u_) {
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    Dfs(v_);
    size[u_] += size[v_]; //u_ 被 v_ 包含
  }
  for (int i = 0, lim = id[u_].size(); i < lim; ++ i) { //枚舉狀態表明的模式串
    sum[id[u_][i]] = size[u_];
  }
}
void Query(char *t_) {
  int now = 0, lth = strlen(t_ + 1);
  for (int i = 1; i <= lth; ++ i) {
    now = tr[now][t_[i] - 'a'];
    ++ size[now];
  }
  for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
  Dfs(0);
  for (int i = 1; i <= n; ++ i) printf("%d\n", sum[i]);
}

複雜度

記 Trie 的節點數量爲 \(n\)\(n\) 的上界爲 \(\sum |s_i|\)
對於時間複雜度,構建 Trie 圖的複雜度爲 \(O(n|\sum|)\),匹配的複雜度爲 \(O(|t| + n)\) 級別。
對於空間複雜度,顯然複雜度爲 \(O(n|\sum|)\)

完整代碼

//知識點:ACAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define LL long long
const int kT = 2e6 + 10;
const int kN = 2e5 + 10;
//=============================================================
int n;
char s[kT];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  std::vector <int> id[kN];
  int node_num, tr[kN][26], sum[kN], fail[kN];
  int e_num, size[kN], head[kN], v[kN], ne[kN];
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  void Dfs(int u_) {
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      size[u_] += size[v_];
    }
    for (int i = 0, lim = id[u_].size(); i < lim; ++ i) {
      sum[id[u_][i]] = size[u_];
    }
  }
  void Insert(int id_, char *s_) {
    int now = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
      now = tr[now][s_[i] - 'a'];
    }
    id[now].push_back(id_);
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      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];
        }
      }
    }
  }
  void Query(char *t_) {
    int now = 0, lth = strlen(t_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      now = tr[now][t_[i] - 'a'];
      ++ size[now];
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    Dfs(0);
    for (int i = 1; i <= n; ++ i) printf("%d\n", sum[i]);
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    scanf("%s", s + 1);
    ACAM::Insert(i, s); 
  }
  ACAM::Build();
  scanf("%s", s + 1);
  ACAM::Query(s);
  return 0;
}

例題

P3796 【模板】AC 自動機(增強版)

\(t\) 組數據,每次給定一個文本串 \(s\)\(n\) 個模式串 \(t_1\sim t_n\),求在文本串中出現次數最多的模式串。
字符串僅由小寫字母構成。模式串互不相同。
\(1\le t\le 50\)\(1\le n\le 150\)\(1\le |t_i|\le 70\)\(1\le |s|\le 10^6\)

板子。

//知識點:ACAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 150 + 5;
const int kT = 1e6 + 10;
const int kNN = 2e5 + 10;
//=============================================================
int n;
char s[kN][71], t[kT];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
struct ACAM {
  int node_num, tr[kNN][26], id[kNN], size[kNN], sum[kNN], fail[kNN];
  int e_num, head[kNN], v[kNN], ne[kNN];
  void Init() {
    node_num = e_num = 0;
    memset(tr, 0, sizeof (tr));
    memset(id, 0, sizeof (id));
    memset(size, 0, sizeof (size));
    memset(head, 0, sizeof (head));
    memset(fail, 0, sizeof (fail));
  }
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  void Dfs(int u_) {
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      size[u_] += size[v_];
    }
    sum[id[u_]] = size[u_];
  }
  void Insert(int id_, char *s_) {
    int now = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
      now = tr[now][s_[i] - 'a'];
    }
    id[now] = id_;
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      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];
        }
      }
    }
  }
  void Query(char *s_) {
    int now = 0, ans = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      now = tr[now][s_[i] - 'a'];
      ++ size[now];
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    Dfs(0);

    for (int i = 1; i <= n; ++ i) Chkmax(ans, sum[i]);
    printf("%d\n", ans);
    for (int i = 1; i <= n; ++ i) {
      if (sum[i] == ans) printf("%s\n", s[i] + 1);
    }
  }
} acam;
//=============================================================
int main() {
  while (true) {
    n = read();
    if (! n) break;
    acam.Init();
    for (int i = 1; i <= n; ++ i) {
      scanf("%s", s[i] + 1);
      acam.Insert(i, s[i]); 
    }
    acam.Build();
    scanf("%s", t + 1);
    acam.Query(t);
  }
  return 0;
}

P3808 【模板】AC 自動機(簡單版)

給定 \(n\) 個模式串 \(s_i\) 和一個文本串 \(t\),求有多少個不一樣的模式串在文本串裏出現過。
字符串僅由小寫字母構成。兩個模式串不一樣當且僅當他們編號不一樣。
\(1\le n,\sum|s_i|\le 10^6\)\(1\le |t|\le 10^6\)
1S,512MB。

題意考慮模式串是否出現,在 Trie 中僅需維護每一個狀態表明多少個模式串,記爲 \(\operatorname{cnt}\)
建出 ACAM,文本串匹配過程當中記錄到達過哪些狀態。以後在 \(\operatorname{fail}\) 樹上 DP,求得哪些狀態在文本串中出現過。將它們的 \(\operatorname{cnt}\) 求和便可。
總時空複雜度 \(O(\sum |s_i|)\) 級別。

//知識點:ACAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define LL long long
const int kN = 1e6 + 10;
//=============================================================
int n;
char s[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  int node_num, tr[kN][26], cnt[kN], fail[kN];
  int e_num, head[kN], v[kN], ne[kN];
  bool size[kN];
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  int Dfs(int u_) {
    int ret = 0;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      ret += Dfs(v_);
      size[u_] |= size[v_];
    }
    return ret + size[u_] * cnt[u_];
  }
  void Insert(int id_, char *s_) {
    int now = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[now][s_[i] - 'a']) tr[now][s_[i] - 'a'] = ++ node_num;
      now = tr[now][s_[i] - 'a'];
    }
    ++ cnt[now];
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      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];
        }
      }
    }
  }
  void Query(char *s_) {
    int now = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      now = tr[now][s_[i] - 'a'];
      size[now] = 1;
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    printf("%d\n", Dfs(0));
  }
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    scanf("%s", s + 1);
    ACAM::Insert(i, s); 
  }
  ACAM::Build();
  scanf("%s", s + 1);
  ACAM::Query(s);
  return 0;
}

「JSOI2007」文本生成器

給定 \(n\) 個只由大寫字母構成的模式串 \(s_1\sim s_n\),給定參數 \(m\)
求有多少個長度爲 \(m\) 的只由大寫字母構成的字符串,知足其中至少有一個給定的模式串,答案對 \(10^4 + 7\) 取模。
\(1\le n\le 60\)\(1\le |s_i|,m\le 100\)
1S,128MB。

?這作法是個套路

先創建 ACAM,建 Trie 圖的時候順便標記全部包含模式串的狀態。記這些狀態構成集合 \(\mathbf{S}\)

發現很差處理含有多個模式串的狀況,考慮補集轉化,答案爲全部串的個數 \(26^{m}\) 減去不含模式串的串個數。
考慮 ACAM 上 DP。設 \(f_{i,j}\) 表示長度爲 \(i\),在 ACAM 上匹配的結束狀態爲 \(j\),不含模式串的字符串的個數。
初始化空串 \(f_{0,0} = 1\)。轉移時枚舉串長,狀態,轉移函數,避免轉移到包含模式串的狀態,有:

\[f_{i,j} = \begin{cases} &\sum\limits_{\operatorname{trans}(u, k) = j} f_{i-1, u} &(j\notin \mathbf{S})\\ &0 &(j\in \mathbf{S}) \end{cases}\]

注意轉移時須要枚舉空串的狀態 0。實現時滾動數組 + 填表便可。
記 Trie 的大小爲 \(|T|\),答案即爲:

\[26^m - \sum_{i=0}^{|T|} f_{m,i} \pmod{10^4+7} \]

總時間複雜度 \(O(m|T||\sum|)\) 級別。


爲何能夠這樣轉移?

能夠發現創建 Trie 圖後,這個轉移過程就至關於字符串的匹配過程。
能夠認爲 DP 過程是經過全部長度爲 \(i-1\) 的字符串在 ACAM 上作匹配,從而獲得長度爲 \(i\) 的字符串對應的狀態。

//知識點:ACAM 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 100 + 10;
const int mod = 1e4 + 7;
//=============================================================
int n, m, ans;
char s[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) {
    w = (w << 3) + (w << 1) + (ch ^ '0');
  }
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace ACAM {
  int node_num, tr[60 * kN][26], fail[60 * kN], f[2][60 * kN];
  bool tag[60 * kN];
  void Insert(char *s_) {
    int u_ = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[u_][s_[i] - 'A']) tr[u_][s_[i] - 'A'] = ++ node_num;
      u_ = tr[u_][s_[i] - 'A'];
    }
    tag[u_] = true;
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int u_ = q.front(); q.pop();
      for (int i = 0; i < 26; ++ i) {
        int v_ = tr[u_][i];
        if (v_) {
          fail[v_] = tr[fail[u_]][i];
          tag[v_] |= tag[fail[v_]];
          q.push(v_);
        } else {
          tr[u_][i] = tr[fail[u_]][i];
        }
      }
    }
  }
  void Query() {    
    ans = f[0][0] = 1;
    for (int i = 1; i <= m; ++ i) ans = 26ll * ans % mod;
    for (int i = 1, now = 1; i <= m; ++ i, now ^= 1) {
      memset(f[now], 0, sizeof (f[now])); //caution:reset
      for (int j = 0; j <= node_num; ++ j) {
        for (int k = 0; k < 26; ++ k) {
          if (tag[tr[j][k]]) continue;
          f[now][tr[j][k]] += f[now ^ 1][j];
          f[now][tr[j][k]] %= mod;
        }
      }
    }
    for (int i = 0; i <= node_num; ++ i) {
      ans = (ans - f[m % 2][i] + mod) % mod;
    }
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    scanf("%s", s + 1);
    ACAM::Insert(s);
  }
  ACAM::Build();
  ACAM::Query();
  printf("%d\n", ans);
  return 0;
}

「BJOI2019」奧術神杖

給定一隻由數字和\(\texttt{.}\)構成的字符串 \(s\)。給定 \(m\) 個特殊串 \(t_{1}\sim t_{m}\)\(t_i\) 的權值爲 \(v_i\)
須要在 \(s\) 中爲\(\texttt{.}\)的位置上填入數字,一種填入方案的價值定義爲:

\[\sqrt[c]{\prod_{i=1}^{c} w_i} \]

其中 \(w\) 表示在該填入方案中,出現過的特殊串的價值的可重集合,其大小爲 \(c\)

每一個位置填入的數字任意,最大化填入方案的價值,並輸出任意一個方案。
\(1\le m,|s|,\sum|t_i|\le 1501\)\(1\le v_i\le 10^9\)
1S,512MB。

對於兩種填入方案,咱們只關心它們價值的相對大小。帶着根號不易比較大小,套路地取個對數,以後化下式子:

\[\begin{aligned} \large \log {\sqrt[c]{\prod_{i=1}^{c} w_i}} =& \dfrac{\log {\left(\prod\limits_{i=1}^{c} w_i\right)}}{c}\\ =& \dfrac{\sum\limits_{i=1}^{c} \log {w_i}}{c} \end{aligned}\]

這是一個顯然的 01 分數規劃的形態,考慮二分答案。存在一種填入方案價值不小於 \(mid\) 的充要條件爲:

\[\begin{aligned} \dfrac{\sum\limits_{i=1}^{c} \log {w_i}}{c}\ge mid \iff \sum\limits_{i=1}^{c}\left(\log {w_i} - mid\right)\ge 0 \end{aligned}\]


考慮 DP 檢查二份量 \(mid\) 是否合法。
具體地,先將特殊串 \(t_i\) 的權值設爲 \(\log v_i - mid\),更新 ACAM 上各狀態的權值,以後在 ACAM 上模擬匹配過程套路 DP。
\(f_{i,j}\) 表示長度爲 \(i\),在 ACAM 上匹配的結束狀態爲 \(j\) 的串的最大價值。
初始化 \(f_{0,0} = 0\),轉移時枚舉串長,狀態,轉移函數。注意某一位不爲\(\texttt{.}\)時轉移函數只能爲串中的字符,則有:

\[f_{i,j} = \begin{cases} &\max\limits_{\operatorname{trans}(u, s_i) = j} f_{i-1, u} + \operatorname{val}_{j} &(s_i\not= \texttt{.})\\ &\max\limits_{\operatorname{trans}(u, k) = j} f_{i-1, u} + \operatorname{val}_{j} &(s_i= \texttt{.}) \end{cases}\]

注意記錄轉移時的前驅與轉移函數,根據前驅還原出方案便可。
總複雜度 \(O(\left(10|s|\cdot\sum |t_i|\right)\log w)\) 級別,\(\log w\) 爲二分次數。

//知識點:ACAM,分數規劃
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define DB double 
const int kN = 3e3 + 10;
const DB kInf = 1e10;
const DB eps = 1e-6;
//=============================================================
int n, m;
char origin[kN], s[kN], ans[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  int node_num = 0, tr[kN][10], fail[kN], cnt[kN], from[kN][kN];
  DB sum[kN], val[kN], f[kN][kN];
  char ch[kN][kN];
  void Insert(char *s_, int val_) {
    int u_ = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[u_][s_[i] - '0']) tr[u_][s_[i] - '0'] = ++ node_num;
      u_ = tr[u_][s_[i] - '0'];
    }
    sum[u_] += log(val_);
    cnt[u_] ++;
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 10; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int u_ = q.front(); q.pop();
      for (int i = 0; i < 10; ++ i) {
        int v_ = tr[u_][i];
        if (v_) {
          fail[v_] = tr[fail[u_]][i];
          sum[v_] += sum[fail[v_]];
          cnt[v_] += cnt[fail[v_]];
          q.push(v_);
        } else {
          tr[u_][i] = tr[fail[u_]][i];
        }
      }
    }
  }
  bool DP(DB mid_) {
    //初始化
    for (int i = 0; i <= node_num; ++ i) val[i] = sum[i] - cnt[i] * mid_;
    for (int i = 0; i <= n; ++ i) {
      for (int j = 0; j <= node_num; ++ j) {
        f[i][j] = -kInf;
      }
    }
    f[0][0] = 0;

    //DP
    for (int i = 0; i < n; ++ i) {
      for (int j = 0; j <= node_num; ++ j) {
        if (f[i][j] == -kInf) continue;
        if (origin[i + 1] == '.') {
          for (int k = 0; k < 10; ++ k) {
            int v_ = tr[j][k];
            if (f[i + 1][v_] < f[i][j] + val[v_]) {
              f[i + 1][v_] = f[i][j] + val[v_];
              from[i + 1][v_] = j;
              ch[i + 1][v_] = k + '0';
            }
          }
        } else {
          int v_ = tr[j][origin[i + 1] - '0'];
          if (f[i + 1][v_] < f[i][j] + val[v_]) {
            f[i + 1][v_] = f[i][j] + val[v_];
            from[i + 1][v_] = j;
            ch[i + 1][v_] = origin[i + 1];
          }
        }
      }
    }

    //尋找最優解
    int pos = 0;
    for (int i = 0; i <= node_num; ++ i) {
      if (f[n][i] > f[n][pos]) pos = i;
    }
    if (f[n][pos] <= 0) return false;
    for (int i = n, j = pos; i; -- i) {
      ans[i] = ch[i][j];
      j = from[i][j];
    }
    return true;
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  scanf("%s", origin + 1);
  for (int i = 1; i <= m; ++ i) {
    scanf("%s", s + 1);
    int val = read();
    ACAM::Insert(s, val);
  }
  ACAM::Build();
  for (DB l = 0, r = log(kInf); r - l >= eps; ) {
    DB mid = (l + r) / 2.0;
    if (ACAM::DP(mid)) {
      l = mid;
    } else {
      r = mid;
    }
  }
  printf("%s", ans + 1);
  return 0; 
}

「SDOI2014」數數

給定一個整數 \(n\),一大小爲 \(m\) 的數字串集合 \(s\)
求不以 \(s\) 中任意一個數字串做爲子串的,不大於 \(n\) 的數字的個數。
\(1\le n\le 10^{1201}\)\(1\le m\le 100\)\(1\le \sum |s_i|\le 1500\)\(n\) 沒有前導零,\(s_i\) 可能存在前導零。
1S,128MB。

數位 DP 相關內容能夠閱讀:「筆記」數位DP

題目要求不以 \(s\) 中任意一個數字串做爲子串,想到這題:「JSOI2007」文本生成器。首先套路地對給定集合的串構建 ACAM,並在 ACAM 上標記全部包含集合內的子串的狀態。
以後考慮在 ACAM 上模擬串匹配的過程作數位 DP。發現前綴所在狀態儲存了前綴的全部信息,能夠將其做爲 dfs 的參數。
Dfs(int now_, int pos_, bool zero_, bool lim_) { 表示前綴匹配到的 ACAM 的狀態爲 \(\operatorname{pos}\) 時,合法的數字的數量。轉移時沿 ACAM 上的轉移函數轉移,避免轉移到被標記的狀態。再簡單記憶化便可。
存在 \(\operatorname{trans}(0, 0) = 0\),這樣直接 dfs 也能順便處理不一樣長度的數字串。
總複雜度 \(O(\log_{10}(n)\sum |s_i|)\) 級別。

//知識點:ACAM,數位 DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 1500 + 10;
const int mod = 1e9 + 7;
//=============================================================
int n, m, ans;
char num[kN], s[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  const int kSigma = 10;
  int node_num, tr[kN][kSigma], last[kN], fail[kN];
  int f[kN][kN];
  bool tag[kN];
  void Insert(char *s_) {
    int u_ = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[u_][s_[i] - '0']) tr[u_][s_[i] - '0'] = ++ node_num;
      u_ = tr[u_][s_[i] - '0'];
      last[u_] = s_[i] - '0';
    }
    tag[u_] = true;
  }
  void Build() {
    std:: queue <int> q;
    for (int i = 0; i < kSigma; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (!q.empty()) {
      int u_ = q.front(); q.pop();
      tag[u_] |= tag[fail[u_]];
      for (int i = 0; i < kSigma; ++ i) {
        int v_ = tr[u_][i];
        if (v_) {
          fail[v_] = tr[fail[u_]][i];
          q.push(v_);
        } else {
          tr[u_][i] = tr[fail[u_]][i];
        }
      }
    }
  }
  int Dfs(int now_, int pos_, bool zero_, bool lim_) {
    if (now_ > n) return 1;
    if (!zero_ && !lim_ && f[now_][pos_] != -1) return f[now_][pos_];
    int ret = 0;
    for (int i = 0, up = lim_ ? num[now_] - '0': 9; i <= up; ++ i) {
      int v_ = tr[pos_][i];
      if (tag[v_]) continue;
      if (zero_ && !i) ret += Dfs(now_ + 1, 0, true, lim_ && i == num[now_] - '0');
      else ret += Dfs(now_ + 1, v_, false, lim_ && i == num[now_] - '0');
      ret %= mod;
    }
    if (!zero_ && !lim_) f[now_][pos_] = ret;
    return ret;
  }
  int DP() {
    memset(f, -1, sizeof (f));
    return Dfs(1, 0, true, true);
  }
}
//=============================================================
int main() {
  scanf("%s", num + 1);
  n = strlen(num + 1);
  m = read();
  for (int i = 1; i <= m; ++ i) {
    scanf("%s", s + 1);
    ACAM::Insert(s);
  }
  ACAM::Build();
  printf("%d\n", ACAM::DP());
  return 0; 
}

「NOI2011」阿狸的打字機

建議先閱讀原題面後再閱讀簡述題面。

經過奇怪的方法給定 \(n\) 個字符串 \(s_1\sim s_n\),給定 \(m\) 次詢問。
每次詢問給定參數 \(x\)\(y\),求在字符串 \(s_y\)\(s_x\) 的出現次數。
\(1\le n,m,|\sum s_i|\le 10^5\)
1S,256MB。

首先能夠發現,題中給出的打字的過程與 Trie 的插入過程相似,由此能夠直接構建出全部串的 Trie。

對 Trie 創建 ACAM 後,先考慮如何暴力查詢。
對於每一次詢問,都將字符串 \(s_y\) 扔到 ACAM 上匹配。每匹配到一個狀態,就暴力上跳考察其在 \(\operatorname{fail}\) 樹上的祖先中是否包含 \(s_x\) 對應狀態。若包含則證實 \(s_x\) 做爲當前匹配部分的一個後綴出現了,貢獻累計即爲答案。
總複雜度能夠達到 \(O(T|\sum| + m|s_i|)\) 級別。其中 \(T\) 爲 ACAM 節點數量,其上限爲 \(\sum |s_i|\)

注意到每次匹配的文本串都是模式串,這說明在匹配過程當中,不會出現失配狀況,且各狀態不重複。即匹配過程當中通過的路徑是 Trie 中的一條自根向下的鏈。

觀察暴力的過程,詢問 \((x,y)\) 的答案即爲祖先包括 \(s_x\) 狀態的 \(s_y\) 的狀態數。
由上述性質,這也能夠理解爲 \(\operatorname{fail}\) 樹上祖先包括 \(s_x\) 的,自根至 \(s_y\) 的 Trie 上的鏈上的節點數量。
更具體地,考慮創建 \(\operatorname{fail}\) 樹,答案爲 \(s_x\) \(\operatorname{fail}\) 的子樹中自根到 \(s_y\) 對應狀態的鏈上的節點數量。


如何實現?對於詢問 \((x,y)\),考慮大力標記 \(s_y\) 對應的全部狀態,再查詢 \(\operatorname{fail}\) 樹上 \(s_x\) 的子樹中被標記點數。上述過程可經過 dfn 序 + 樹狀數組完成。

若是對每次詢問都作一次上面的過程,顯然是很是浪費的。考慮離線全部詢問,在每次詢問的狀態 \(s_y\) 上打一個詢問 \(s_x\) 的標記。
以後在 Trie 上 dfs,每第一次訪問到一個節點,就令樹狀數組中對應 dfn 位置 \(+1\),表示標記該節點。從該節點回溯時再 \(-1\)
能夠發現,dfs 到狀態 \(u\) 時,被標記的節點剛好組成了自根至 \(s_y\) 的 Trie 上的鏈上的節點。則訪問到 \(u\) 便可直接查詢離線下來的詢問。

總時間負責度 \(O(T|\sum| + m\log T)\),其中 \(T\) 爲 ACAM 節點數量,其上限爲 \(\sum |s_i|\)

實現細節詳見代碼,注意映射關係。

//知識點:ACAM,BIT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, ans[kN], pos[kN];
char s[kN];
std::vector <int> query1[kN], query2[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace BIT {
  #define low(x) (x&-x)
  int Lim, t[kN];
  void Init(int lim_) {
    Lim = lim_;
  }
  void Insert(int pos_, int val_) {
    for (int i = pos_; i <= Lim; i += low(i)) {
      t[i] += val_;
    }
  }
  int Sum(int pos_) {
     int ret = 0;
    for (int i = pos_; i; i -= low(i)) {
      ret += t[i];
    }
    return ret;
  }
  int Query(int l_, int r_) {
    return Sum(r_) - Sum(l_ - 1);
  }
  #undef low
}
namespace ACAM {
  int node_num, fa[kN], tr[kN][26], fail[kN];
  int e_num, head[kN], v[kN], ne[kN];
  int dfn_num, dfn[kN], size[kN];
  std::vector <int> trans[kN]; //原 Trie 樹上的轉移。由於創建了 Trie 圖,須要把它記錄下來,
  void Read(char *s_) { //按照讀入創建 Trie
    int now = 0;
    for(int i = 1, lim = strlen(s_ + 1); i <= lim; ++ i) {
      if (s_[i] == 'P') {
        pos[++ n] = now;
      } else if (s_[i] == 'B') {
        now = fa[now];
      } else {
        if (!tr[now][s_[i] - 'a']) {
          tr[now][s_[i] - 'a'] = ++ node_num;
          trans[now].push_back(node_num);
          fa[node_num] = now;
        }
        now = tr[now][s_[i] - 'a'];
      }
    }
  }
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  void Dfs(int u_) {
    dfn[u_] = ++ dfn_num;
    size[u_] = 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      size[u_] += size[v_];
    }
  }
  void Build(char *s_) {
    Read(s_);
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int now = q.front(); q.pop();
      for (int i = 0; i < 26; ++ i) {
        if (tr[now][i]) {
          fail[tr[now][i]] = tr[fail[now]][i];
          q.push(tr[now][i]);
        } else {
          tr[now][i] = tr[fail[now]][i];
        }
      }
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    Dfs(0);
    BIT::Init(node_num + 1);
  }
  void Query(int u_) { //dfs 回答詢問到 u_
    BIT::Insert(dfn[u_], 1); //標記
    for (int i = 0, lim = query1[u_].size(); i < lim; ++ i) { //枚舉此時能夠回答的詢問
      int x = query1[u_][i], id = query2[u_][i]; //查詢 x 的子樹中標記點的個數
      ans[id] = BIT::Query(dfn[x], dfn[x] + size[x] - 1);
    }
    for (int i = 0, lim = trans[u_].size(); i < lim; ++ i) Query(trans[u_][i]);
    BIT::Insert(dfn[u_], -1); //去除標記
  }
}
//=============================================================
int main() { 
  scanf("%s", s + 1);
  ACAM::Build(s);
  int m = read();
  for (int i = 1; i <= m; ++ i) { //離線詢問
    int x = read(), y = read();
    query1[pos[y]].push_back(pos[x]);
    query2[pos[y]].push_back(i);
  }
  ACAM::Query(0);
  for (int i = 1; i <= m; ++ i) printf("%d\n", ans[i]);
  return 0; 
}

寫在最後

參考資料:

AC 自動機 - OI Wiki
AC自動機學習筆記 - ouuan的博客

相關文章
相關標籤/搜索