AC自动机相关:

$fail$树:

$fail$树上以最长$border$关系形成父子关系,我们定一个节点对应的串为根到该节点的路径。

对于任意一个非根节点$x$,定$y = fa_{x}$,那$y$对应的串就是$x$对应的串的最长$border$,也就是说如果母串能走到$x$,那母串中一定存在一个子串对应了$y$,而且是当前母串匹配到当前位置的一个后缀。

求每个模式串在母串中出现的次数:

这应该算是AC自动机最基本的问题。

把母串在自动机上跑一遍,显然所有被访问过的节点都是母串的子串,但以当前匹配位置为后缀的模式串可能不仅仅只有一个,还有它所有的$border$。一个很好的性质就是,$fail$树上某个节点的祖先链就恰好完全对应了该节点的所有$border$。我们用$cnt_{i}$表示$i$点的祖先链上有效节点的个数,那只要每次匹配到某个节点$x$时,把它的$cnt_{x}$加上贡献就行了。

如果要支持动态加减模式串,由于修改一个节点的有效性只会对它的子树中所有节点造成影响,只要维护$Dfs$序上的信息即可。

AC自动机上的$Dp$问题

通常情况下,这类$Dp$问题的数据范围不大,设计状态的基本套路通常有两维,一维是母串匹配到的位置,一维是在自动机上的位置,转移基本上都是枚举字符。

$\star$ 一个简单的例子,就是给出一个数$n$和$m$个字符串,问长度为$n$的字符串有多少个满足这$m$个串都是它的子串。题目链接

由于$m$非常小,我们直接状压起来,表示该节点对应的串包含了哪些子串,$Dp$有两种方式,$Dfs$和$Bfs$都可以,如果$Dfs$的话状态的含义通常表示为当前状态到结束状态的方案数。

不过由于这题要输出方案,用$Bfs$的做法不太容易,还是用$Dfs$的吧。

  1. #include <cstdio>
  2. #include <queue>
  3. #include <cstring>
  4.  
  5. using namespace std;
  6.  
  7. typedef long long LL;
  8. const int N = , M = ;
  9.  
  10. int n, m, ST, tot;
  11. char sr[];
  12. int ch[M][], fail[M], fir[M];
  13. queue<int> Q;
  14.  
  15. LL dp[N][M][], ans;
  16.  
  17. void Ins(char *s, int id) {
  18. int pos = ;
  19. for (int i = ; s[i]; ++i) {
  20. int nx = s[i] - 'a';
  21. if (!ch[pos][nx]) {
  22. ch[pos][nx] = ++tot;
  23. memset(ch[tot], , sizeof ch[tot]);
  24. fir[tot] = fail[tot] = ;
  25. }
  26. pos = ch[pos][nx];
  27. }
  28. fir[pos] |= << id;
  29. }
  30.  
  31. void Build() {
  32. Q.push();
  33. for (; !Q.empty(); ) {
  34. int x = Q.front(); Q.pop();
  35. for (int i = ; i < ; ++i) {
  36. int v = ch[x][i];
  37. if (!v) ch[x][i] = ch[fail[x]][i]; else Q.push(v);
  38. if (x && v) fail[v] = ch[fail[x]][i], fir[v] |= fir[fail[v]];
  39. }
  40. }
  41. }
  42.  
  43. LL Dfs(int x, int p, int s) {
  44. if (~dp[x][p][s]) return dp[x][p][s];
  45. if (x == n) {
  46. dp[x][p][s] = (LL) (s == ST);
  47. return dp[x][p][s];
  48. }
  49. dp[x][p][s] = ;
  50. for (int i = ; i < ; ++i) {
  51. int np = ch[p][i];
  52. dp[x][p][s] += Dfs(x + , np, s | fir[np]);
  53. }
  54. return dp[x][p][s];
  55. }
  56.  
  57. void F_(int x, int p, int s) {
  58. if (x == n) {
  59. sr[x] = '\0';
  60. printf("%s\n", sr);
  61. return;
  62. }
  63. for (int i = ; i < ; ++i) {
  64. int np = ch[p][i];
  65. if (dp[x + ][np][s | fir[np]]) {
  66. sr[x] = 'a' + i;
  67. F_(x + , np, s | fir[np]);
  68. sr[x] = '\0';
  69. }
  70. }
  71. }
  72.  
  73. void Clear() {
  74. memset(dp, -, sizeof dp);
  75. memset(ch[], , sizeof ch[]);
  76. tot = ans = ;
  77. }
  78.  
  79. int main() {
  80. for (int tc = ; scanf("%d%d", &n, &m) == && n + m > ; ) {
  81. Clear();
  82. ST = ( << m) - ;
  83. for (int i = ; i < m; ++i) {
  84. scanf("%s", sr);
  85. Ins(sr, i);
  86. }
  87. Build();
  88. ans = Dfs(, , );
  89. printf("Case %d: %lld suspects\n", ++tc, ans);
  90. if (ans <= ) {
  91. memset(sr, , sizeof sr);
  92. F_(, , );
  93. }
  94. }
  95.  
  96. return ;
  97. }

后缀自动机相关:

$parent$树:

$parent$树上以后缀关系形成父子关系,对于任意非根节点有$dep_{i} - dep_{fa_{i}}$为节点$i$包含的子串个数。

求一个串的不重复子串个数:

这是后缀自动机上的一个经典问题,很多时候它都会作为解决一个问题的子问题。事实上这个问题很容易想到,每一个子串都别表现在了自动机上的一个节点,所有相同的子串只会被表现一次,重复的将算在$right$集合中了。每个节点包含的不重复子串个数就是$max_{i} - min_{i} + 1$连续的一段,把所有节点包含的不重复子串数量相加就行了。

$ans = \sum\limits_{i = 1}^{tot} dep_{i} - dep_{fa_{i}}$。

将所有子串按照字典序排序:

由于字典序是比较前缀的,通常情况我们把反串建出后缀自动机,那一个节点上表示的子串都有同样的后缀,对应了原串的前缀,那我们只要对反串进行后缀意义上的排序就可以了。很显然,一个节点中表示的所有子串一定在后缀字典序中是连续的。于是,我们对于$parent$树上每一个节点$x$,假设$y = fa_{x}$,$x$中最短的子串比$y$中最长的子串多的那个字符$a$一定会在$y$中最长串的前一格,那我们就定$son_{y,a} = x$。显然,$son$表示的就是一棵树,这棵树的$Dfs$序就是我们要求的对于每个节点而言的后缀字典序,因为我们在对它进行$Dfs$时会优先选择下一个字符较小的串。

以下给出具体建树的实现:

  1. void Dfs(int t) {
  2. id[++tp] = t;
  3. for (int i = ; i < ; ++i) {
  4. if (son[t][i]) Dfs(son[t][i]);
  5. }
  6. }
  7.  
  8. void Build() {
  9. for (int i = ; i <= tot; ++i) ++bit[dep[i]];
  10. for (int i = ; i <= tot; ++i) bit[i] += bit[i - ];
  11. for (int i = tot; i >= ; --i) id[bit[dep[i]]--] = i;
  12. for (int i = tot; i >= ; --i) {
  13. int x = id[i];
  14. son[fa[x]][s[ed[x] - dep[fa[x]]] - 'a'] = x;
  15. }
  16. Dfs();
  17. }

(注:其中$ed_{i}$表示节点$i$表示的子串末尾字符的位置)

求出字典序第$k$小子串:

此处分两种,一种是算重复子串,一种是不算重复子串,本质上没有区别,如果算重复子串只要对每个节点多乘上$right$集合大小就可以了。

有了上一个模型,我们只要在$Dfs$后的序列上二分就可以了,维护一个前缀的$sum_{i}$表示前$i$个节点表示的子串有多少个,二分可以找到第$k$小子串所在的节点,就能知道是那个子串了。以上是不计重复子串的,如果算上重复子串,由于每个节点上的$right$集合大小是一样的,所以在计算子串个数时乘上$right$集合大小即可,另一个问题,如果算重复子串,那在求出该子串的具体位置的时候一般需要另一个二分确定它的长度。

确定某子串在所有子串中字典序排名:

以$[l,r]$的形式给出一个子串,设$pos_{i}$为第$i$个字符串被插入时新建的节点,由于互为后缀的节点在$parent$树上形成了一条到根的链,显然子串$[l,r]$将会出现在$pos_{r}$的祖先链上,我们就可以用树上倍增找到包含$[l,r]$的节点了。我们可以这么做,是因为子串长度随节点深度递减而单调递减,并且子串长度范围没有交集。

$\star$ 下面的一道题就是上述的有关子串字典序的一个应用,掌握了这个套路就没有什么难度了,仅此以作实例。题目链接

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5.  
  6. using namespace std;
  7.  
  8. typedef long long LL;
  9. const int N = ;
  10.  
  11. int n, Qi, tp, gans, ans;
  12. char s[N];
  13. LL sum[N];
  14.  
  15. int tot, lst, ch[N][], dep[N], fa[N], rig[N];
  16. int id[N], bit[N], ed[N], son[N][];
  17. inline int New_(int _d, int _f, int _r, int _e) {
  18. dep[++tot] = _d; fa[tot] = _f; rig[tot] = _r; ed[tot] = _e;
  19. return tot;
  20. }
  21. inline void Ins(int a, int i) {
  22. int np = New_(dep[lst] + , , , i), p = lst;
  23. lst = np;
  24. for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  25. if (!p) return void(fa[np] = );
  26. int q = ch[p][a];
  27. if (dep[p] + == dep[q]) return void(fa[np] = q);
  28. int y = New_(dep[p] + , fa[q], , ed[q]);
  29. memcpy(ch[y], ch[q], sizeof ch[y]);
  30. fa[q] = fa[np] = y;
  31. for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
  32. }
  33. void Build() {
  34. for (int i = ; i <= tot; ++i) ++bit[dep[i]];
  35. for (int i = ; i <= tot; ++i) bit[i] += bit[i - ];
  36. for (int i = tot; i >= ; --i) id[bit[dep[i]]--] = i;
  37. for (int i = tot; i >= ; --i) {
  38. int x = id[i];
  39. rig[fa[x]] += rig[x];
  40. ed[fa[x]] = ed[x]; // a question for why it is used
  41. son[fa[x]][s[ed[x] - dep[fa[x]]] - 'a'] = x;
  42. }
  43. }
  44.  
  45. void Dfs(int t) {
  46. id[++tp] = t;
  47. for (int i = ; i < ; ++i) {
  48. if (son[t][i]) Dfs(son[t][i]);
  49. }
  50. }
  51.  
  52. LL Get(int l, int r) {
  53. if (l > r) return ;
  54. return (LL) (l + r) * (r - l + ) / ;
  55. }
  56.  
  57. int Solve(LL k) {
  58. int nl = , nr = tot, po = -;
  59. for (int md; nl <= nr; ) {
  60. md = (nl + nr) >> ;
  61. if (sum[md] >= k) {
  62. po = md; nr = md - ;
  63. } else {
  64. nl = md + ;
  65. }
  66. }
  67. if (po == -) throw;
  68. k -= sum[po - ];
  69. int L = dep[fa[id[po]]] + , R = dep[id[po]], re = -;
  70. nl = L; nr = R;
  71. for (int md; nl <= nr; ) {
  72. md = (nl + nr) >> ;
  73. if (k <= rig[id[po]] * Get(L, md)) {
  74. re = md; nr = md - ;
  75. } else {
  76. nl = md + ;
  77. }
  78. }
  79. if (re == -) throw;
  80. k -= rig[id[po]] * Get(L, re - );
  81. k = (k - ) % re + ;
  82. return s[ed[id[po]] - k + ];
  83. }
  84.  
  85. int main() {
  86. scanf("%s", s + );
  87. n = strlen(s + );
  88. std::reverse(s + , s + + n);
  89. tot = lst = ;
  90. for (int i = ; s[i]; ++i) Ins(s[i] - 'a', i);
  91. Build();
  92. Dfs();
  93. for (int i = ; i <= tot; ++i) {
  94. sum[i] = sum[i - ] + rig[id[i]] * Get(dep[fa[id[i]]] + , dep[id[i]]);
  95. }
  96.  
  97. scanf("%d", &Qi);
  98. for (LL p, m, k; Qi; --Qi) {
  99. scanf("%lld%lld", &p, &m);
  100. k = gans * p % m + ;
  101. ans = Solve(k);
  102. printf("%c\n", ans);
  103. gans += ans;
  104. }
  105.  
  106. return ;
  107. }

 用线段树合并维护每个节点的$right$集合:

很多时候我们先要利用$right$集合的有关信息,可以所有$right$集合的大小加起来是$O(n^{2})$的,可幸的是$right$集合拥有一个重要的优越性质,对于$parent$树上每一个节点,它的$right$集合一定是它父亲的$right$集合的真子集,并且和它的兄弟的$right$集合不相交。于是我们可以用线段树合并来维护,每次从叶子到根把信息合并到父亲上去,需要注意的是我们不想在合并时改变原有的信息,所以合并时需要新建节点,故时空复杂度都是$O(nlogn)$的。

$\star$ 下面的一道题就是有关线段树合并维护$right$集合的应用,具体思想就是二分答案串长,倍增找到子串的节点后询问$right$集合中是否存在。题目链接

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5.  
  6. using namespace std;
  7.  
  8. const int N = , LOG = ;
  9.  
  10. int n, m;
  11. int Rt[N], gr[LOG][N];
  12. char s[N];
  13.  
  14. namespace SE {
  15. const int M = N * ;
  16. int tot, lc[M], rc[M], sum[M];
  17. void Modify(int &t, int l, int r, int x) {
  18. if (!t) t = ++tot;
  19. ++sum[t];
  20. if (l == r) return;
  21. int md = (l + r) >> ;
  22. if (x <= md) Modify(lc[t], l, md, x);
  23. else Modify(rc[t], md + , r, x);
  24. }
  25. int Merge(int x, int y) {
  26. if (!x || !y) return x + y;
  27. int z = ++tot;
  28. lc[z] = Merge(lc[x], lc[y]);
  29. rc[z] = Merge(rc[x], rc[y]);
  30. sum[z] = sum[lc[z]] + sum[rc[z]];
  31. return z;
  32. }
  33. int Query(int t, int l, int r, int L, int R) {
  34. int md = (l + r) >> , re = ;
  35. if (L <= l && r <= R) return sum[t];
  36. if (L <= md) re += Query(lc[t], l, md, L, R);
  37. if (R > md) re += Query(rc[t], md + , r, L, R);
  38. return re;
  39. }
  40. }
  41.  
  42. int tot = , lst = , ch[N][], dep[N], fa[N];
  43. int bit[N], id[N], pos[N];
  44. inline int New_(int _dep, int _fa) {
  45. dep[++tot] = _dep; fa[tot] = _fa;
  46. return tot;
  47. }
  48. void Ins(int a, int i) {
  49. int np = New_(dep[lst] + , ), p = lst;
  50. lst = np; pos[i] = tot;
  51. SE::Modify(Rt[tot], , n, i);
  52. for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  53. if (!p) return void(fa[np] = );
  54. int q = ch[p][a];
  55. if (dep[q] == dep[p] + ) return void(fa[np] = q);
  56. int y = New_(dep[p] + , fa[q]);
  57. memcpy(ch[y], ch[q], sizeof ch[y]);
  58. fa[q] = fa[np] = y;
  59. for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
  60. }
  61. void Build() {
  62. for (int i = ; i <= tot; ++i) ++bit[dep[i]];
  63. for (int i = ; i <= tot; ++i) bit[i] += bit[i - ];
  64. for (int i = ; i <= tot; ++i) id[bit[dep[i]]--] = i;
  65. for (int i = ; i <= tot; ++i) {
  66. int x = id[i]; gr[][x] = fa[x];
  67. for (int j = ; j < LOG; ++j) gr[j][x] = gr[j - ][gr[j - ][x]];
  68. }
  69. for (int i = tot; i >= ; --i) {
  70. int x = id[i], y = fa[x];
  71. Rt[y] = SE::Merge(Rt[x], Rt[y]);
  72. }
  73. }
  74.  
  75. int Check(int x, int md, int l, int r) {
  76. if (l > r) return ;
  77. for (int i = LOG - ; ~i; --i) {
  78. if (gr[i][x] && dep[gr[i][x]] >= md) x = gr[i][x];
  79. }
  80. return SE::Query(Rt[x], , n, l, r) > ;
  81. }
  82.  
  83. int main() {
  84. scanf("%d%d", &n, &m);
  85. scanf("%s", s + );
  86. reverse(s + , s + + n);
  87. for (int i = ; i <= n; ++i) Ins(s[i] - 'a', i);
  88. Build();
  89.  
  90. for (int a, b, c, d; m; --m) {
  91. scanf("%d%d%d%d", &a, &b, &c, &d);
  92. a = n - a + ; b = n - b + ; swap(a, b);
  93. c = n - c + ; d = n - d + ; swap(c, d);
  94. int nl = , nr = d - c + , re = ;
  95. for (int md; nl <= nr; ) {
  96. md = (nl + nr) >> ;
  97. if (Check(pos[d], md, a + md - , b)) {
  98. re = md; nl = md + ;
  99. } else {
  100. nr = md - ;
  101. }
  102. }
  103. printf("%d\n", re);
  104. }
  105.  
  106. return ;
  107. }

$\star$ 有了以上的作为基础,我们就能解决下面这个问题了,有两问。

  1. 给定$k1,k2$,求不重复子串中字典序第$k1$小的子串,并在所有与它相同的子串中找到从左到右第$k2$个,以$[l,r]$的形式返回答案。
  2. 以$[l,r]$的形式给定子串,求它在所有不重复子串中字典序的排名$k1$,以及在所有与它相同的子串中从左到右的排名$k2$,返回$k1,k2$。

对于第一问:

我们根据$son$树的$Dfs$序可以知道字典序第$k1$小的子串所在的节点$x$,然后我们想要知道$x$的$right$集合的第$k2$个元素在哪个位置,在线段树上二分即可。

对于第二问:

我们在$parent$树上倍增就能找到子串$[l,r]$所在的节点$x$,然后我们想要知道$r$这个位置在$x$的$right$集合中排第几,在线段树上二分即可。

我想要说的是,有关字典序、子串等的问题都是比较套路的问题,掌握技巧就可以了。

这是这个问题的相关实现:原题地址

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5.  
  6. using namespace std;
  7.  
  8. typedef long long LL;
  9. const int N = , LOG = ;
  10.  
  11. int n, m, tp;
  12. int Rt[N], gr[LOG][N];
  13. char s[N], ssr[];
  14. LL sum[N];
  15.  
  16. inline void Read(LL &x) {
  17. x = ; static char c;
  18. for (c = getchar(); c < '' || c > ''; c = getchar());
  19. for (; c >= '' && c <= ''; x = (x << ) + (x << ) + c - '', c = getchar());
  20. }
  21.  
  22. namespace SE {
  23. const int M = N * ;
  24. int tot, lc[M], rc[M], sum[M];
  25. void Modify(int &t, int l, int r, int x) {
  26. if (!t) t = ++tot;
  27. ++sum[t];
  28. if (l == r) return;
  29. int md = (l + r) >> ;
  30. if (x <= md) Modify(lc[t], l, md, x);
  31. else Modify(rc[t], md + , r, x);
  32. }
  33. int Merge(int x, int y) {
  34. if (!x || !y) return x + y;
  35. int z = ++tot;
  36. lc[z] = Merge(lc[x], lc[y]);
  37. rc[z] = Merge(rc[x], rc[y]);
  38. sum[z] = sum[lc[z]] + sum[rc[z]];
  39. return z;
  40. }
  41. int Query_1(int t, int l, int r, int k) {
  42. if (l == r) return l;
  43. int md = (l + r) >> ;
  44. if (sum[lc[t]] >= k) return Query_1(lc[t], l, md, k);
  45. return Query_1(rc[t], md + , r, k - sum[lc[t]]);
  46. }
  47. int Query_2(int t, int l, int r, int k) {
  48. if (l == r) return sum[t];
  49. int md = (l + r) >> ;
  50. if (k <= md) return Query_2(lc[t], l, md, k);
  51. return sum[lc[t]] + Query_2(rc[t], md + , r, k);
  52. }
  53. }
  54.  
  55. int tot = , lst = , ch[N][], dep[N], rig[N], fa[N], ed[N];
  56. int bit[N], id[N], rk[N], pos[N], son[N][];
  57. inline int New_(int _dep, int _fa, int _ri, int _e) {
  58. dep[++tot] = _dep; fa[tot] = _fa; rig[tot] = _ri; ed[tot] = _e;
  59. return tot;
  60. }
  61. void Ins(int a, int i) {
  62. int np = New_(dep[lst] + , , , i), p = lst;
  63. lst = np; pos[i] = tot;
  64. SE::Modify(Rt[tot], , n, i);
  65. for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  66. if (!p) return void(fa[np] = );
  67. int q = ch[p][a];
  68. if (dep[q] == dep[p] + ) return void(fa[np] = q);
  69. int y = New_(dep[p] + , fa[q], , ed[q]);
  70. memcpy(ch[y], ch[q], sizeof ch[y]);
  71. fa[q] = fa[np] = y;
  72. for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
  73. }
  74. void Dfs(int x) {
  75. id[++tp] = x;
  76. for (int i = ; i < ; ++i) {
  77. if (son[x][i]) Dfs(son[x][i]);
  78. }
  79. }
  80. void Build() {
  81. for (int i = ; i <= tot; ++i) ++bit[dep[i]];
  82. for (int i = ; i <= tot; ++i) bit[i] += bit[i - ];
  83. for (int i = ; i <= tot; ++i) id[bit[dep[i]]--] = i;
  84. for (int i = ; i <= tot; ++i) {
  85. int x = id[i]; gr[][x] = fa[x];
  86. for (int j = ; j < LOG; ++j) gr[j][x] = gr[j - ][gr[j - ][x]];
  87. }
  88. for (int i = tot; i >= ; --i) {
  89. int x = id[i], y = fa[x];
  90. rig[y] += rig[x];
  91. Rt[y] = SE::Merge(Rt[x], Rt[y]);
  92. son[y][s[ed[x] - dep[y]] - 'a'] = x;
  93. }
  94. Dfs();
  95. if (tp != tot) { cerr << "not tp equal!" << endl; throw; }
  96. for (int i = ; i <= tot; ++i) {
  97. rk[id[i]] = i;
  98. sum[i] = sum[i - ] + dep[id[i]] - dep[fa[id[i]]];
  99. }
  100. }
  101.  
  102. pair<LL, LL> Solve_1(LL k1, int k2) {
  103. int nl = , nr = tot, x = -;
  104. for (int md; nl <= nr; ) {
  105. md = (nl + nr) >> ;
  106. if (k1 <= sum[md]) {
  107. x = md; nr = md - ;
  108. } else {
  109. nl = md + ;
  110. }
  111. }
  112. if (x == -) { cerr << "not found po!" << endl; throw; }
  113. k1 -= sum[x - ];
  114. x = id[x];
  115. if (k2 > rig[x]) { cerr << "k2" << endl; throw; }
  116. k2 = rig[x] - k2 + ;
  117. int ps = SE::Query_1(Rt[x], , n, k2);
  118. int len = k1 + dep[fa[x]];
  119. return make_pair(ps - len + , ps);
  120. }
  121. pair<LL, LL> Solve_2(int l, int r) {
  122. int x = pos[r], le = r - l + ;
  123. for (int i = LOG - ; ~i; --i) {
  124. if (gr[i][x] && dep[gr[i][x]] >= le) x = gr[i][x];
  125. }
  126. int re = SE::Query_2(Rt[x], , n, r);
  127. LL rnk = sum[rk[x] - ] + le - dep[fa[x]];
  128. return make_pair(rnk, rig[x] - re + );
  129. }
  130.  
  131. int main() {
  132. scanf("%s", s + );
  133. n = strlen(s + );
  134. reverse(s + , s + + n);
  135. for (int i = ; i <= n; ++i) Ins(s[i] - 'a', i);
  136. Build();
  137.  
  138. scanf("%d", &m);
  139. pair<LL, LL> re;
  140. for (LL l, r; m; --m) {
  141. scanf("%s", ssr);
  142. Read(l); Read(r);
  143. if (ssr[] == 'S') {
  144. re = Solve_1(l, r);
  145. re.first = n - re.first + ;
  146. re.second = n - re.second + ;
  147. swap(re.first, re.second);
  148. } else {
  149. l = n - l + ; r = n - r + ; swap(l, r);
  150. re = Solve_2(l, r);
  151. }
  152. printf("%lld %lld\n", re.first, re.second);
  153. }
  154.  
  155. return ;
  156. }

 求两子串的$LCS$($LCP$):

子串的最长公共后缀在$parent$树上是一个经典问题,我们已经知道了如何找到一个子串所在的节点,那么答案节点就是两个子串所在节点在$parent$树上的$LCA$。

对于$LCP$,只要对反串做就行了。

后缀自动机在区间上的单调性问题:

通常情况下,有关后缀的区间问题总是会有单调性,就是子串的存在性。我们的区间就是子串,显然,如果一个区间是某个串的子串,那它的子区间就一定是该串的子串。

我们来看一个简单的问题,给出串$S,T$,询问$S$中有多少子串是$T$的子串。

根据上述的单调性,对于每一个作为子串右端点的位置$i$,都存在一个极左的位置$l_{i}$满足所有比$l_{i}$小的位置都不合法,所有大于等于$l_{i}$的位置都合法,所以用$two \; pointers$能解决问题。我们往往用自动机上的一个节点$p$表示当前我们维护的区间(子串),每次$nr$准备向右移一格时,相当于在$p$上尝试转移,如果存在转移边,那$[nl,nr+1]$也就是说一个合法的子串,$nl$不动;否则就需要让$nl$右移一格,这就相当于变成了比$[nl,nr+1]$恰好短一的一个后缀。这里有注意的地方就是,当每次短一时有可能串长减小到它父亲的长度范围,也就是此时串长$len = dep_{fa_{p}}$,这时要让$p$跳一次父亲。

$\star$ 这里给出一道有关单调性的问题。题目链接

可以观察到,我们进行复制操作时,设$j$为复制的起点,则$[j + 1, i]$必须是$[1,j]$的子串,而这个是存在上述我们讨论的单调性的。

可以列出$Dp$的方程:$dp_{i} = max_{j = k}^{i - 1} \; \{ \; dp_{j} + (i - j) * A + 2 * B \; \}$,以及和$dp_{i - 1} + cost_{s_{i}}$取最小值,其中$k$为最小的能作为起点的位置。

要求出每一个$i$的$k$,可以用上述的方法维护,只不过当前的自动机只有$[1,k]$而已,要支持动态插入字符,可能会改变$p$的节点位置,所以在维护$p$时记得更新。

那我们只要维护区间最值就行了,这里用由于$k$是单调递增的,用单调队列维护就行了。

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <iostream>
  4. #include <algorithm>
  5.  
  6. using namespace std;
  7.  
  8. typedef long long LL;
  9. const int N = ;
  10.  
  11. int tc, n, kp, le;
  12. char s[N];
  13. int q[N], cost[];
  14. LL dp[N], A, B;
  15.  
  16. int tot, lst, ch[N][], dep[N], fa[N];
  17. inline int New_(int _dep, int _fa) {
  18. dep[++tot] = _dep; fa[tot] = _fa;
  19. memset(ch[tot], , sizeof ch[tot]);
  20. return tot;
  21. }
  22. inline void Ins(int a) {
  23. int np = New_(dep[lst] + , ), p = lst;
  24. lst = np;
  25. for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  26. if (!p) return void(fa[np] = );
  27. int q = ch[p][a];
  28. if (dep[p] + == dep[q]) return void(fa[np] = q);
  29. int y = New_(dep[p] + , fa[q]);
  30. memcpy(ch[y], ch[q], sizeof ch[y]);
  31. fa[q] = fa[np] = y;
  32. if (kp == q && le <= dep[y]) kp = y;
  33. for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
  34. }
  35.  
  36. void Clear() {
  37. memset(ch[], , sizeof ch[]);
  38. tot = lst = ;
  39. }
  40.  
  41. int main() {
  42. scanf("%d", &tc);
  43. for (int tm = ; tm <= tc; ++tm) {
  44. Clear();
  45. scanf("%s", s + );
  46. n = strlen(s + );
  47. for (int i = ; i < ; ++i) {
  48. scanf("%d", &cost[i]);
  49. }
  50. scanf("%lld%lld", &A, &B);
  51. int nl = , nr = ; kp = ;
  52. for (int i = , j = ; i <= n; ++i) {
  53. int nc = s[i] - 'a';
  54. dp[i] = dp[i - ] + cost[nc];
  55. while (j <= i && !ch[kp][nc]) {
  56. if (i - j - == dep[fa[kp]]) kp = fa[kp];
  57. le = i - j - ;
  58. Ins(s[j] - 'a'); ++j;
  59. }
  60. if (j <= i) kp = ch[kp][nc];
  61. while (nl <= nr && q[nl] < j - ) ++nl;
  62. if (nl <= nr) dp[i] = min(dp[i], dp[q[nl]] + (i - q[nl]) * A + * B);
  63. while (nl <= nr && dp[q[nr]] - q[nr] * A >= dp[i] - i * A) --nr;
  64. q[++nr] = i;
  65. }
  66. printf("Case #%d: %lld\n", tm, dp[n]);
  67. }
  68.  
  69. return ;
  70. }

【专题】字符串专题小结(AC自动机 + 后缀自动机)的更多相关文章

  1. bzoj 3796: Mushroom追妹纸 AC自动机+后缀自动机+dp

    题目大意: 给定三个字符串s1,s2,s3,求一个字符串w满足: w是s1的子串 w是s2的子串 s3不是w的子串 w的长度应尽可能大 题解: 首先我们可以用AC自动机找出s3在s1,s2中出现的位置 ...

  2. BZOJ2754: [SCOI2012]喵星球上的点名(AC自动机/后缀自动机)

    Description a180285幸运地被选做了地球到喵星球的留学生.他发现喵星人在上课前的点名现象非常有趣.   假设课堂上有N个喵星人,每个喵星人的名字由姓和名构成.喵星球上的老师会选择M个串 ...

  3. AC自动机&后缀自动机

    理解的不够深 故只能以此来加深理解 .我这个人就是蠢没办法 学长讲的题全程蒙蔽.可能我字符串就是菜吧,哦不我这个人就是菜吧. AC自动机的名字 AC 取自一个大牛 而自动机就比较有讲究了 不是寻常的东 ...

  4. BZOJ 3926: [Zjoi2015]诸神眷顾的幻想乡 广义后缀自动机 后缀自动机 字符串

    https://www.lydsy.com/JudgeOnline/problem.php?id=3926 广义后缀自动机是一种可以处理好多字符串的一种数据结构(不像后缀自动机只有处理一到两种的时候比 ...

  5. BZOJ 3998: [TJOI2015]弦论 后缀自动机 后缀自动机求第k小子串

    http://www.lydsy.com/JudgeOnline/problem.php?id=3998 后缀自动机应用的一个模板?需要对len进行一个排序之后再统计每个出现的数量,维护的是以该字符串 ...

  6. BZOJ4032[HEOI2015]最短不公共子串——序列自动机+后缀自动机+DP+贪心

    题目描述 在虐各种最长公共子串.子序列的题虐的不耐烦了之后,你决定反其道而行之. 一个串的“子串”指的是它的连续的一段,例如bcd是abcdef的子串,但bde不是. 一个串的“子序列”指的是它的可以 ...

  7. CCF NOI Online 2021 提高组 T2 积木小赛 (子序列自动机+后缀自动机,O(n^2))

    题面 Alice 和 Bob 最近热衷于玩一个游戏--积木小赛. Alice 和 Bob 初始时各有 n 块积木从左至右排成一排,每块积木都被标上了一个英文小写字母. Alice 可以从自己的积木中丢 ...

  8. 字符串[未AC](后缀自动机):HEOI 2016 str

    超级恶心,先后用set维护right,再用主席树维护,全部超时,本地测是AC的.放心,BZOJ上还是1S限制,貌似只有常数优化到一定境界的人才能AC吧. 总之我是精神胜利了哦耶QAQ #include ...

  9. HDU - 6208 The Dominator of Strings HDU - 6208 AC自动机 || 后缀自动机

    https://vjudge.net/problem/HDU-6208 首先可以知道最长那个串肯定是答案 然后,相当于用n - 1个模式串去匹配这个主串,看看有多少个能匹配. 普通kmp的话,每次都要 ...

随机推荐

  1. 我的第一个上线小程序,案例实战篇二——LayaAir游戏开始界面开发

    不知不觉我的第一个小程序已经上线一周了,uv也稳定的上升着. 很多人说我的小程序没啥用,我默默一笑,心里说:“它一直敦促我学习,敦促我进步”.我的以一个小程序初衷是经验分享,目前先把经验分享到博客园, ...

  2. Netty源码分析第6章(解码器)---->第4节: 分隔符解码器

    Netty源码分析第六章: 解码器 第四节: 分隔符解码器 基于分隔符解码器DelimiterBasedFrameDecoder, 是按照指定分隔符进行解码的解码器, 通过分隔符, 可以将二进制流拆分 ...

  3. VMware vCenter Converter迁移Linux系统虚拟机

    (一)简介VMware vCenter Converter Standalone,是一种用于将虚拟机和物理机转换为 VMware 虚拟机的可扩展解决方案.此外,还可以在 vCenter Server ...

  4. python-python爬取豆果网(菜谱信息)

    #-*- coding = utf-8 -*- #获取豆果网图片 import io from bs4 import BeautifulSoup import requests #爬取菜谱的地址 ur ...

  5. EOS博彩合约设计

    集中博彩游戏合约设计 一.功能接口 1. 质押deposit 由用户发起,用户将个人账户中token质押给平台,从而可以进入平台去参与平台活动. 2. 赎回withdraw 由用户发起,在用户结束平台 ...

  6. JDK8 metaspace调优

    从JDK8开始,永久代(PermGen)的概念被废弃掉了,取而代之的是一个称为Metaspace的存储空间.Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大 ...

  7. C++ 函数 函数的重载 有默认参数的函数

    函数的重载 C++允许用同一函数名定义多个函数,这些函数的参数个数和参数类型不同.这就是函数的重载(function overloading). int max1(int a,int b, int c ...

  8. 第35次Scrum会议(11/23)【欢迎来怼】

    一.小组信息 队名:欢迎来怼小组成员队长:田继平成员:李圆圆,葛美义,王伟东,姜珊,邵朔,阚博文小组照片 二.开会信息 时间:2017/11/23 17:03~17:24,总计21min.地点:东北师 ...

  9. 实验3 --俄罗斯方块 with 20135335郝爽

    一.   实验内容 (一)敏捷开发与XP 内容:1.敏捷开发(Agile Development)是一种以人为核心.迭代.循序渐进的开发方法. 2.极限编程(eXtreme Programming,X ...

  10. Hibernate笔记①--myeclipse制动配置hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库. Hibernate可以应用在任何使用JD ...