【专题】字符串专题小结(AC自动机 + 后缀自动机)

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$的吧。

#include <cstdio>
#include <queue>
#include <cstring>

using namespace std;

typedef long long LL;
const int N = 27, M = 135;

int n, m, ST, tot;
char sr[29];
int ch[M][27], fail[M], fir[M];
queue<int> Q;

LL dp[N][M][1037], ans;

void Ins(char *s, int id) {
  int pos = 0;
  for (int i = 0; s[i]; ++i) {
    int nx = s[i] - 'a';
    if (!ch[pos][nx]) {
      ch[pos][nx] = ++tot;
      memset(ch[tot], 0, sizeof ch[tot]);
      fir[tot] = fail[tot] = 0;
    }
    pos = ch[pos][nx];
  }
  fir[pos] |= 1 << id;
}

void Build() {
  Q.push(0);
  for (; !Q.empty(); ) {
    int x = Q.front(); Q.pop();
    for (int i = 0; i < 26; ++i) {
      int v = ch[x][i];
      if (!v) ch[x][i] = ch[fail[x]][i]; else Q.push(v);
      if (x && v) fail[v] = ch[fail[x]][i], fir[v] |= fir[fail[v]];
    }
  }
}

LL Dfs(int x, int p, int s) {
  if (~dp[x][p][s]) return dp[x][p][s];
  if (x == n) {
    dp[x][p][s] = (LL) (s == ST);
    return dp[x][p][s];
  }
  dp[x][p][s] = 0;
  for (int i = 0; i < 26; ++i) {
    int np = ch[p][i];
    dp[x][p][s] += Dfs(x + 1, np, s | fir[np]);
  }
  return dp[x][p][s];
}

void F_(int x, int p, int s) {
  if (x == n) {
    sr[x] = '\0';
    printf("%s\n", sr);
    return;
  }
  for (int i = 0; i < 26; ++i) {
    int np = ch[p][i];
    if (dp[x + 1][np][s | fir[np]]) {
      sr[x] = 'a' + i;
      F_(x + 1, np, s  | fir[np]);
      sr[x] = '\0';
    }
  }
}

void Clear() {
  memset(dp, -1, sizeof dp);
  memset(ch[0], 0, sizeof ch[0]);
  tot = ans = 0;
}

int main() {
  for (int tc = 0; scanf("%d%d", &n, &m) == 2 && n + m > 0; ) {
    Clear();
    ST = (1 << m) - 1;
    for (int i = 0; i < m; ++i) {
      scanf("%s", sr);
      Ins(sr, i);
    }
    Build();
    ans = Dfs(0, 0, 0);
    printf("Case %d: %lld suspects\n", ++tc, ans);
    if (ans <= 42) {
      memset(sr, 0, sizeof sr);
      F_(0, 0, 0);
    }
  }
  
  return 0;
}
View Code

 

 

后缀自动机相关:

$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$时会优先选择下一个字符较小的串。

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

void Dfs(int t) {
  id[++tp] = t;
  for (int i = 0; i < 26; ++i) {
    if (son[t][i]) Dfs(son[t][i]);
  }
}

void Build() {
  for (int i = 1; i <= tot; ++i) ++bit[dep[i]];
  for (int i = 1; i <= tot; ++i) bit[i] += bit[i - 1];
  for (int i = tot; i >= 1; --i) id[bit[dep[i]]--] = i;
  for (int i = tot; i >= 1; --i) {
    int x = id[i];
    son[fa[x]][s[ed[x] - dep[fa[x]]] - 'a'] = x;
  }
  Dfs(1);
}
View Code

(注:其中$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$ 下面的一道题就是上述的有关子串字典序的一个应用,掌握了这个套路就没有什么难度了,仅此以作实例。题目链接

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
 
using namespace std;
 
typedef long long LL;
const int N = 2000005;
 
int n, Qi, tp, gans, ans;
char s[N];
LL sum[N];
 
int tot, lst, ch[N][26], dep[N], fa[N], rig[N];
int id[N], bit[N], ed[N], son[N][26];
inline int New_(int _d, int _f, int _r, int _e) {
  dep[++tot] = _d; fa[tot] = _f; rig[tot] = _r; ed[tot] = _e;
  return tot;
}
inline void Ins(int a, int i) {
  int np = New_(dep[lst] + 1, 0, 1, i), p = lst;
  lst = np;
  for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  if (!p) return void(fa[np] = 1);
  int q = ch[p][a];
  if (dep[p] + 1 == dep[q]) return void(fa[np] = q);
  int y = New_(dep[p] + 1, fa[q], 0, ed[q]);
  memcpy(ch[y], ch[q], sizeof ch[y]);
  fa[q] = fa[np] = y;
  for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
}
void Build() {
  for (int i = 1; i <= tot; ++i) ++bit[dep[i]];
  for (int i = 1; i <= tot; ++i) bit[i] += bit[i - 1];
  for (int i = tot; i >= 1; --i) id[bit[dep[i]]--] = i;
  for (int i = tot; i >= 1; --i) {
    int x = id[i];
    rig[fa[x]] += rig[x];
    ed[fa[x]] = ed[x]; // a question for why it is used
    son[fa[x]][s[ed[x] - dep[fa[x]]] - 'a'] = x;
  }
}
 
void Dfs(int t) {
  id[++tp] = t;
  for (int i = 0; i < 26; ++i) {
    if (son[t][i]) Dfs(son[t][i]);
  }
}
 
LL Get(int l, int r) {
  if (l > r) return 0;
  return (LL) (l + r) * (r - l + 1) / 2;
}
 
int Solve(LL k) {
  int nl = 1, nr = tot, po = -1;
  for (int md; nl <= nr; ) {
    md = (nl + nr) >> 1;
    if (sum[md] >= k) {
      po = md; nr = md - 1;
    } else {
      nl = md + 1;
    }
  }
  if (po == -1) throw;
  k -= sum[po - 1];
  int L = dep[fa[id[po]]] + 1, R = dep[id[po]], re = -1;
  nl = L; nr = R;
  for (int md; nl <= nr; ) {
    md = (nl + nr) >> 1;
    if (k <= rig[id[po]] * Get(L, md)) {
      re = md; nr = md - 1;
    } else {
      nl = md + 1;
    }
  }
  if (re == -1) throw;
  k -= rig[id[po]] * Get(L, re - 1);
  k = (k - 1) % re + 1;
  return s[ed[id[po]] - k + 1];
}
 
int main() {
  scanf("%s", s + 1);
  n = strlen(s + 1);
  std::reverse(s + 1, s + 1 + n);
  tot = lst = 1;
  for (int i = 1; s[i]; ++i) Ins(s[i] - 'a', i);
  Build();
  Dfs(1);
  for (int i = 1; i <= tot; ++i) {
    sum[i] = sum[i - 1] + rig[id[i]] * Get(dep[fa[id[i]]] + 1, dep[id[i]]);
  }
 
  scanf("%d", &Qi);
  for (LL p, m, k; Qi; --Qi) {
    scanf("%lld%lld", &p, &m);
    k = gans * p % m + 1;
    ans = Solve(k);
    printf("%c\n", ans);
    gans += ans;
  }
 
  return 0;
}
View Code

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

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

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

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
 
using namespace std;
 
const int N = 200005, LOG = 19;
 
int n, m;
int Rt[N], gr[LOG][N];
char s[N];
 
namespace SE {
  const int M = N * 20;
  int tot, lc[M], rc[M], sum[M];
  void Modify(int &t, int l, int r, int x) {
    if (!t) t = ++tot;
    ++sum[t];
    if (l == r) return;
    int md = (l + r) >> 1;
    if (x <= md) Modify(lc[t], l, md, x);
    else Modify(rc[t], md + 1, r, x);
  }
  int Merge(int x, int y) {
    if (!x || !y) return x + y;
    int z = ++tot;
    lc[z] = Merge(lc[x], lc[y]);
    rc[z] = Merge(rc[x], rc[y]);
    sum[z] = sum[lc[z]] + sum[rc[z]];
    return z;
  }
  int Query(int t, int l, int r, int L, int R) {
    int md = (l + r) >> 1, re = 0;
    if (L <= l && r <= R) return sum[t];
    if (L <= md) re += Query(lc[t], l, md, L, R);
    if (R > md) re += Query(rc[t], md + 1, r, L, R);
    return re;
  }
}
 
int tot = 1, lst = 1, ch[N][26], dep[N], fa[N];
int bit[N], id[N], pos[N];
inline int New_(int _dep, int _fa) {
  dep[++tot] = _dep; fa[tot] = _fa;
  return tot;
}
void Ins(int a, int i) {
  int np = New_(dep[lst] + 1, 0), p = lst;
  lst = np; pos[i] = tot;
  SE::Modify(Rt[tot], 1, n, i);
  for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  if (!p) return void(fa[np] = 1);
  int q = ch[p][a];
  if (dep[q] == dep[p] + 1) return void(fa[np] = q);
  int y = New_(dep[p] + 1, fa[q]);
  memcpy(ch[y], ch[q], sizeof ch[y]);
  fa[q] = fa[np] = y;
  for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
}
void Build() {
  for (int i = 1; i <= tot; ++i) ++bit[dep[i]];
  for (int i = 1; i <= tot; ++i) bit[i] += bit[i - 1];
  for (int i = 1; i <= tot; ++i) id[bit[dep[i]]--] = i;
  for (int i = 1; i <= tot; ++i) {
    int x = id[i]; gr[0][x] = fa[x];
    for (int j = 1; j < LOG; ++j) gr[j][x] = gr[j - 1][gr[j - 1][x]];
  }
  for (int i = tot; i >= 2; --i) {
    int x = id[i], y = fa[x];
    Rt[y] = SE::Merge(Rt[x], Rt[y]);
  }
}
 
int Check(int x, int md, int l, int r) {
  if (l > r) return 0;
  for (int i = LOG - 1; ~i; --i) {
    if (gr[i][x] && dep[gr[i][x]] >= md) x = gr[i][x];
  }
  return SE::Query(Rt[x], 1, n, l, r) > 0;
}
 
int main() {
  scanf("%d%d", &n, &m);
  scanf("%s", s + 1);
  reverse(s + 1, s + 1 + n);
  for (int i = 1; i <= n; ++i) Ins(s[i] - 'a', i);
  Build();
 
  for (int a, b, c, d; m; --m) {
    scanf("%d%d%d%d", &a, &b, &c, &d);
    a = n - a + 1; b = n - b + 1; swap(a, b);
    c = n - c + 1; d = n - d + 1; swap(c, d);
    int nl = 1, nr = d - c + 1, re = 0;
    for (int md; nl <= nr; ) {
      md = (nl + nr) >> 1;
      if (Check(pos[d], md, a + md - 1, b)) {
        re = md; nl = md + 1;
      } else {
        nr = md - 1;
      }
    }
    printf("%d\n", re);
  }
   
  return 0;
}
View Code

 $\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$集合中排第几,在线段树上二分即可。

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

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

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
 
using namespace std;
 
typedef long long LL;
const int N = 2000005, LOG = 22;
 
int n, m, tp;
int Rt[N], gr[LOG][N];
char s[N], ssr[13];
LL sum[N];
 
inline void Read(LL &x) {
  x = 0; static char c;
  for (c = getchar(); c < '0' || c > '9'; c = getchar());
  for (; c >= '0' && c <= '9'; x = (x << 3) + (x << 1) + c - '0', c = getchar());
}
 
namespace SE {
  const int M = N * 40;
  int tot, lc[M], rc[M], sum[M];
  void Modify(int &t, int l, int r, int x) {
    if (!t) t = ++tot;
    ++sum[t];
    if (l == r) return;
    int md = (l + r) >> 1;
    if (x <= md) Modify(lc[t], l, md, x);
    else Modify(rc[t], md + 1, r, x);
  }
  int Merge(int x, int y) {
    if (!x || !y) return x + y;
    int z = ++tot;
    lc[z] = Merge(lc[x], lc[y]);
    rc[z] = Merge(rc[x], rc[y]);
    sum[z] = sum[lc[z]] + sum[rc[z]];
    return z;
  }
  int Query_1(int t, int l, int r, int k) {
    if (l == r) return l;
    int md = (l + r) >> 1;
    if (sum[lc[t]] >= k) return Query_1(lc[t], l, md, k);
    return Query_1(rc[t], md + 1, r, k - sum[lc[t]]);
  }
  int Query_2(int t, int l, int r, int k) {
    if (l == r) return sum[t];
    int md = (l + r) >> 1;
    if (k <= md) return Query_2(lc[t], l, md, k);
    return sum[lc[t]] + Query_2(rc[t], md + 1, r, k);
  }
}
 
int tot = 1, lst = 1, ch[N][26], dep[N], rig[N], fa[N], ed[N];
int bit[N], id[N], rk[N], pos[N], son[N][26];
inline int New_(int _dep, int _fa, int _ri, int _e) {
  dep[++tot] = _dep; fa[tot] = _fa; rig[tot] = _ri; ed[tot] = _e;
  return tot;
}
void Ins(int a, int i) {
  int np = New_(dep[lst] + 1, 0, 1, i), p = lst;
  lst = np; pos[i] = tot;
  SE::Modify(Rt[tot], 1, n, i);
  for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  if (!p) return void(fa[np] = 1);
  int q = ch[p][a];
  if (dep[q] == dep[p] + 1) return void(fa[np] = q);
  int y = New_(dep[p] + 1, fa[q], 0, ed[q]);
  memcpy(ch[y], ch[q], sizeof ch[y]);
  fa[q] = fa[np] = y;
  for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
}
void Dfs(int x) {
  id[++tp] = x;
  for (int i = 0; i < 26; ++i) {
    if (son[x][i]) Dfs(son[x][i]);
  }
}
void Build() {
  for (int i = 1; i <= tot; ++i) ++bit[dep[i]];
  for (int i = 1; i <= tot; ++i) bit[i] += bit[i - 1];
  for (int i = 1; i <= tot; ++i) id[bit[dep[i]]--] = i;
  for (int i = 1; i <= tot; ++i) {
    int x = id[i]; gr[0][x] = fa[x];
    for (int j = 1; j < LOG; ++j) gr[j][x] = gr[j - 1][gr[j - 1][x]];
  }
  for (int i = tot; i >= 2; --i) {
    int x = id[i], y = fa[x];
    rig[y] += rig[x];
    Rt[y] = SE::Merge(Rt[x], Rt[y]);
    son[y][s[ed[x] - dep[y]] - 'a'] = x;
  }
  Dfs(1);
  if (tp != tot) { cerr << "not tp equal!" << endl; throw; }
  for (int i = 1; i <= tot; ++i) {
    rk[id[i]] = i;
    sum[i] = sum[i - 1] + dep[id[i]] - dep[fa[id[i]]];
  }
}
 
pair<LL, LL> Solve_1(LL k1, int k2) {
  int nl = 1, nr = tot, x = -1;
  for (int md; nl <= nr; ) {
    md = (nl + nr) >> 1;
    if (k1 <= sum[md]) {
      x = md; nr = md - 1;
    } else {
      nl = md + 1;
    }
  }
  if (x == -1) { cerr << "not found po!" << endl; throw; }
  k1 -= sum[x - 1];
  x = id[x];
  if (k2 > rig[x]) { cerr << "k2" << endl; throw; }
  k2 = rig[x] - k2 + 1;
  int ps = SE::Query_1(Rt[x], 1, n, k2);
  int len = k1 + dep[fa[x]];
  return make_pair(ps - len + 1, ps);
}
pair<LL, LL> Solve_2(int l, int r) {
  int x = pos[r], le = r - l + 1;
  for (int i = LOG - 1; ~i; --i) {
    if (gr[i][x] && dep[gr[i][x]] >= le) x = gr[i][x];
  }
  int re = SE::Query_2(Rt[x], 1, n, r);
  LL rnk = sum[rk[x] - 1] + le - dep[fa[x]];
  return make_pair(rnk, rig[x] - re + 1);
}
 
int main() {
  scanf("%s", s + 1);
  n = strlen(s + 1);
  reverse(s + 1, s + 1 + n);
  for (int i = 1; i <= n; ++i) Ins(s[i] - 'a', i);
  Build();
  
  scanf("%d", &m);
  pair<LL, LL> re;
  for (LL l, r; m; --m) {
    scanf("%s", ssr);
    Read(l); Read(r);
    if (ssr[0] == 'S') {
      re = Solve_1(l, r);
      re.first = n - re.first + 1;
      re.second = n - re.second + 1;
      swap(re.first, re.second);
    } else {
      l = n - l + 1; r = n - r + 1; swap(l, r);
      re = Solve_2(l, r);
    }
    printf("%lld %lld\n", re.first, re.second);
  }
  
  return 0;
}
View Code

 求两子串的$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$是单调递增的,用单调队列维护就行了。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;
const int N = 200005;

int tc, n, kp, le;
char s[N];
int q[N], cost[27];
LL dp[N], A, B;

int tot, lst, ch[N][26], dep[N], fa[N];
inline int New_(int _dep, int _fa) {
  dep[++tot] = _dep; fa[tot] = _fa;
  memset(ch[tot], 0, sizeof ch[tot]);
  return tot;
}
inline void Ins(int a) {
  int np = New_(dep[lst] + 1, 0), p = lst;
  lst = np;
  for (; p && !ch[p][a]; p = fa[p]) ch[p][a] = np;
  if (!p) return void(fa[np] = 1);
  int q = ch[p][a];
  if (dep[p] + 1 == dep[q]) return void(fa[np] = q);
  int y = New_(dep[p] + 1, fa[q]);
  memcpy(ch[y], ch[q], sizeof ch[y]);
  fa[q] = fa[np] = y;
  if (kp == q && le <= dep[y]) kp = y;
  for (; p && ch[p][a] == q; p = fa[p]) ch[p][a] = y;
}

void Clear() {
  memset(ch[1], 0, sizeof ch[1]);
  tot = lst = 1;
}

int main() {
  scanf("%d", &tc);
  for (int tm = 1; tm <= tc; ++tm) {
    Clear();
    scanf("%s", s + 1);
    n = strlen(s + 1);
    for (int i = 0; i < 26; ++i) {
      scanf("%d", &cost[i]);
    }
    scanf("%lld%lld", &A, &B);
    int nl = 1, nr = 0; kp = 1;
    for (int i = 1, j = 1; i <= n; ++i) {
      int nc = s[i] - 'a';
      dp[i] = dp[i - 1] + cost[nc];
      while (j <= i && !ch[kp][nc]) {
    if (i - j - 1 == dep[fa[kp]]) kp = fa[kp];
    le = i - j - 1;
    Ins(s[j] - 'a'); ++j;
      }
      if (j <= i) kp = ch[kp][nc];
      while (nl <= nr && q[nl] < j - 1) ++nl;
      if (nl <= nr) dp[i] = min(dp[i], dp[q[nl]] + (i - q[nl]) * A + 2 * B);
      while (nl <= nr && dp[q[nr]] - q[nr] * A >= dp[i] - i * A) --nr;
      q[++nr] = i;
    }
    printf("Case #%d: %lld\n", tm, dp[n]);
  }

  return 0;
}
View Code

 

posted @ 2018-07-28 22:39  Dance_Of_Faith  阅读(390)  评论(0编辑  收藏  举报