「笔记」广义后缀自动机

写在前面

阅读之前请首先确保您已经熟练掌握了后缀自动机:「笔记」后缀自动机
本文仅做广义后缀自动机处理多模式串问题的简单整理,更加详细的讲解建议阅读:《后缀自动机在字典树上的拓展》 - 刘研绎【学习笔记】字符串—广义后缀自动机 - 辰星凌

介绍

广义 SAM 是一种用于维护 Trie 的子串信息的 SAM 的简单变体。

将多个模式串插入到 Trie 后,即可使用广义 SAM 维护多模式串的信息。这是广义 SAM 最广泛的应用之一,本文的重点也在于此。其基本思想是将多串的信息进行压缩,使得 SAM 在仍满足节点数最少的同时 包含所有子串的信息。此时 SAM 中的一个状态可能同时代表多个串中相应的子串。

离线构造

一种显然的做法是先使用多个模式串构造 Trie 树,再在 Trie 上构建 SAM。
与一般的 SAM 相同,也可以使用增量法进行广义 SAM 的构建。即将结点 \(u\) 插入到广义 SAM 中状态 \(\operatorname{fa}_u\) 之后。有 dfs 与 bfs 两种不同的写法。
注意此处的 Insert 函数需要返回对应 SAM 结点的编号。

namespace Trie {
  int tr[kN][26], last[kN]; //tr:Trie 的转移函数,last:结点对应的 SAM 的状态
  void Dfs(int u_) {
    for (int i = 0; i < 26; ++ i) {
      if (tr[u_][i]) {
        int v_ = tr[u_][i];
        last[v_] = SAM::Insert(last[u_], i + 'a');
        Dfs(v_);
      }
    }
  }
  void BuildDfs() {
    last[1] = 1;
    Dfs(1);
  }
}
namespace Trie {
  //tr:Trie 的转移函数,last:结点对应的 SAM 的状态
  //fa:Trie 上的父亲,ch:储存的字符
  int ch[kN], fa[kN], tr[kN][26], last[kN];
  void BuildBfs() {
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) q.push(tr[1][i]);
    last[1] = 1;
    while (! q.empty()) {
      int u_ = q.front(); q.pop();
      last[u_] = SAM::Insert(last[fa[u_]], ch[u_]);
      for (int i = 0; i < 26; ++ i) {
        if (tr[u_][i]) q.push(tr[u_][i]);
      }
    }
  }
}

两种构造方法的时间复杂度不同。对于 bfs 写法,其复杂度为 \(O(|T||\sum| + |T|)\) 级别,其中 \(|T|\) 为 Trie 树的节点个数。
而 dfs 的复杂度为 \(O\left(|T||\sum| + \sum\limits_{u\in \mathbf{leaves}} \operatorname{dep}_u\right)\),其上界为 \(O\left(|T||\sum| + |T|^2\right)\),在类似下图的结构中 dfs 的复杂度可以卡到上界:

详细的证明在论文中有介绍,比较繁琐此处不展开。

在线构造

可以发现,dfs 的离线构造中,每次都会深入到叶节点后再回溯。这个过程等价于将 Trie 树中所有自根到叶的字符串依次插入到 SAM 中,在此过程中使用了字符串的公共前缀进行了优化。
将 dfs 写法修改后可以得到一种不需要建立 Trie 树,在线构建 SAM 的方法。

int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) return q;
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 

  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}

  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
int main() {
  while (T --) {
    scanf("%s", S + 1);
    int n = strlen(S + 1), last = 1; //注意
    for (int i = 1; i <= n; ++ i) last = Insert(S[i] - 'a', last);
  }
  return 0;
}
 

看起来很长?其实这份代码只是在一般 SAM 的基础上添加了开头特判部分。
若该特判成立,说明新插入的串已经在 SAM 中出现过,需要考虑信息的合并。

p = last_, q = ch[last_][c_]
若满足 \(\operatorname{len}(p) + 1 = \operatorname{len}(q)\),说明在 \(p\) 之后插入新字符后得到的状态,在 SAM 中已经有一个与之完全相同的状态 \(q\) 了,可以直接对应过去并 return 即可。此时状态 \(q\) 内含有多个串的 \(\operatorname{endpos}\) 信息。
否则,说明状态 \(q\) 内包含了新状态的信息,也包含着原串其他信息,考虑分裂状态 \(q\),产生新状态 \(newq\) 来储存新串新状态的信息,将原串的其它信息保留在 \(q\) 中。分裂过程与 SAM 分裂状态原理类似。最后返回代表新串新状态的 \(newq\) 作为新的 last_

代码

Luogu

给定 \(n\) 个字符串 \(S_1\sim S_n\),求它们中所有的本质不同子串个数。
\(1\le n\le 4\times 10^5, 1\le \sum|S_i|\le 10^6\)
2S,512MB。

经典问题,建出广义 SAM 后,答案即:

\[\sum \operatorname{len}(i) - \operatorname{len}(\operatorname{link}(i)) \]

//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
ll ans;
char S[kMaxn];
int node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
//=============================================================
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;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) return q;
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
//=============================================================
int main() {
  int T = read();
  while (T --) {
    scanf("%s", S + 1);
    int n = strlen(S + 1), last = 1;
    for (int i = 1; i <= n; ++ i) {
      last = Insert(S[i] - 'a', last);
    }
  }
  for (int i = 2; i <= node_num; ++ i) {
    ans += len[i] - len[link[i]];
  }
  printf("%lld\n", ans);
  return 0; 
}

复杂度

状态数上界 \(O(2|T|)\),转移函数数量上界为 \(O(|T||A|)\)
离线 bfs 构建的时间复杂度为 \(O(|T||A|+|T|)\),在线构建的时间复杂度为 \(O(|T||A|+ \sum\limits_{u\in \mathbf{leaves}} \operatorname{dep}_u)\)
证明详见论文。

例题

「ZJOI2015」诸神眷顾的幻想乡

给定一棵 \(n\) 个节点的树,每个节点都有一个字符 \(c_i\)
可任意选择两个点 \(u,v\),路径 \(u\rightarrow v\) 上的字符构成一个字符串(\(v\rightarrow u\) 构成的字符串可能不同)。
求所有可以构成的 不同的字符串的数量。
\(1\le n\le 10^5, 1\le c_i\le 10\)最多存在 \(20\) 个叶节点
1S,512MB。

发现 任意 选择两个节点构成的所有字符串,等价于选择两个 叶节点 构成的字符串的 所有子串

如果给定的是一条链,就非常好做了,直接将整个串正反插入广义 SAM 中,求 \(\sum\operatorname{len}(i) - \operatorname{len}(\operatorname{link}(i))\) 即可。
想到把弯曲的路径掰♂直,发现任何一条路径,都会在叶节点到其他叶节点 构成的路径中出现。此时发现题中给了一个很特别的性质:最多存在 \(20\) 个叶节点。考虑暴力枚举选择两个 叶节点 构成的字符串,并将它们插入广义 SAM 中,求 \(\sum\operatorname{len}(i) - \operatorname{len}(\operatorname{link}(i))\) 即为答案。

复杂度大概是 \(O(\text{leaves}\_\text{num}\times n)\),可过。

//知识点:广义 SAM,暴力
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
const int kMaxm = 10;
//=============================================================
ll ans;
char S[kMaxn];
bool is_leaves[kMaxn];
int leaves_num, leaves[50];
int edge_num, c[kMaxn], into[kMaxn], head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
int node_num = 1, ch[kMaxn << 5][kMaxm], len[kMaxn << 5], link[kMaxn << 5];
//=============================================================
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 AddEdge(int u_, int v_) {
  v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) return q;
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
void Dfs(int u_, int fa_, int dep_) {
  S[dep_] = c[u_];
  if (is_leaves[u_] && dep_ != 1) {
    int last = 1;
    for (int i = 1; i <= dep_; ++ i) last = Insert(S[i], last);
    return ;
  }
  for (int i = head[u_]; i; i = ne[i]) {
    if (v[i] != fa_) Dfs(v[i], u_, dep_ + 1);
  }
}
//=============================================================
int main() {
  int n = read(), Marisa = read();
  for (int i = 1; i <= n; ++ i) c[i] = read();
  for (int i = 1; i < n; ++ i) {
    int u = read(), v = read(); 
    AddEdge(u, v), AddEdge(v, u);
    into[u] ++, into[v] ++;
  }
  for (int i = 1; i <= n; ++ i) {
    if (into[i] == 1) {
      leaves[++ leaves_num] = i;
      is_leaves[i] = true; 
    }
  }
  for (int i = 1; i <= leaves_num; ++ i) {
    Dfs(leaves[i], 0, 1); 
  }
  for (int i = 2; i <= node_num; ++ i) {
    ans += len[i] - len[link[i]]; 
  }
  printf("%lld\n", ans);
  return 0; 
}

P4081 [USACO17DEC]Standing Out from the Herd P

给定 \(n\) 个仅包含小写字母的字符串 \(S_1\sim S_n\)
定义字符串 \(S_i\) 的 「独特值」为只属于该串的本质不同的非空子串的个数。
求字符串 \(S_1\sim S_n\) 的「独特值」。
\(1\le n\le 10^5, 1\le \sum|S_i|\le 10^5\)
1S,128MB。

考虑在线建立广义 SAM,构建时记 \(\operatorname{only}_i\) 表示状态 \(i\) 包含哪一个串的信息。特别地,若某状态包含多个串信息,则\(\operatorname{only}_i = - 1\)
parent 树上 DP 更新祖先信息得到 \(\operatorname{only}\) 后,直接遍历所有状态,若存在 \(\operatorname{only}_i \not= -1\),则令 \(ans _{\operatorname{only}_i}\) 加上 \(\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))\)即可。
复杂度 \(O(\sum|S_i|)\) 级别。

//知识点:广义 SAM
/*
By:Luckyblock  
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define ll long long
const int kMaxn = 2e5 + 10;
const int kMaxm = 26;
//=============================================================
std :: string S[kMaxn];
int only[kMaxn << 1], top[kMaxn << 1], sum[kMaxn << 1];
int num, node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
int edge_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1]; 
//=============================================================
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 AddEdge(int u_, int v_) {
  v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) {
      only[q] = - 1;
      return q;
    }
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    only[newq] = num;
    return newq;
  }
  int p = last_, now = ++ node_num;
  only[now] = num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
void Dfs1(int u_) {
  for (int i = head[u_]; i; i = ne[i]) {
    Dfs1(v[i]);
    if (only[u_] == - 1) continue ;
    if (! only[u_]) {
      only[u_] = only[v[i]]; 
    } else if (only[u_] != only[v[i]]) {
      only[u_] = - 1; 
    }
  }
}
void Dfs2(int u_, int top_) {
  if (! top_ && only[u_] != - 1) top_ = u_;
  top[u_] = top_;
  if (top_) sum[u_] += len[u_] - len[link[u_]];
  for (int i = head[u_]; i; i = ne[i]) {
    Dfs2(v[i], top_);
    sum[u_] += sum[v[i]]; 
  }
}
void Work(std :: string S_) {
  std :: vector <int> node;
  ll ans = 0;
  int n = S_.length(), now = 1;
  for (int i = 0; i < n; ++ i) {
    now = ch[now][S_[i] - 'a'];
    if (only[now] != - 1) node.push_back(top[now]);
  }
  std :: sort(node.begin(), node.end());
  for (int i = 0, n = node.size(); i < n; ++ i) {
    if (i != 0) {
      if (node[i] == node[i - 1]) continue;
    }
    ans += sum[node[i]];
  }
  printf("%lld\n", ans);
}
//=============================================================
int main() {
  int T = read();
  for (num = 1; num <= T; ++ num) {
    std :: cin >> S[num];
    int n = S[num].length(), last = 1;
    for (int j = 0; j < n; ++ j) last = Insert(S[num][j] - 'a', last);
  }
  for (int i = 2; i <= node_num; ++ i) AddEdge(link[i], i);
  Dfs1(1), Dfs2(1, 0);
  for (int i = 1; i <= T; ++ i) Work(S[i]);
  return 0; 
}

「HAOI2016」找相同字符

给定两字符串 \(S_1, S_2\),求出在两字符串中各取一个子串,使得这两个子串相同的方案数。
两方案不同当且仅当这两个子串中有一个位置不同。
\(1\le |S_1|, |S_2|\le 2\times 10^5\)
1S,256MB。

用两个字符串构造广义 SAM,维护每个状态维护了几个串的 \(\operatorname{endpos}\)
当一个状态同时维护了两个串的 \(\operatorname{endpos}\),则该状态及其 parent 树上的祖先 所代表的串,均为公共子串。

\(size(u,0/1)\) 表示状态 \(u\) 维护了串 1/2 的 \(\operatorname{endpos}\) 的个数,有:

\[\sum size(i,0)\times size(i,1)\times (\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i)) \]

具体地,每插入串 \(i\) 的一个新字符,就对该字符对应的状态的 \(size(i) +1\),在 parent 树上求子树 \(size\) 和,最后枚举状态更新答案。

总复杂度 \(O(|S|)\) 级别,为保证复杂度需要使用计数排序。

//知识点:SAM
/*
By:Luckyblock
试了试变量写法,挺清爽的。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
ll ans;
char S[kMaxn];
int size[kMaxn][2], id[kMaxn], cnt[kMaxn];
int num, node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
//=============================================================
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;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) return q;
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
//=============================================================
int main() {
  for (; num <= 1; ++ num) {
    scanf("%s", S + 1);
    int n = strlen(S + 1), last = 1;
    for (int i = 1; i <= n; ++ i) {
      last = Insert(S[i] - 'a', last);
      size[last][num] = 1;
    }
  }
  for (int i = 1; i <= node_num; ++ i) cnt[len[i]] ++;
  for (int i = 1; i <= node_num; ++ i) cnt[i] += cnt[i - 1];
  for (int i = 1; i <= node_num; ++ i) id[cnt[len[i]] --] = i;
  for (int i = node_num; i >= 2; -- i) {
    int now = id[i];
    size[link[now]][0] += size[now][0];
    size[link[now]][1] += size[now][1];
  }
  for (int i = 2; i <= node_num; ++ i) {
    ans += 1ll * size[i][0] * size[i][1] * (len[i] - len[link[i]]);
  }
  printf("%lld\n", ans);
  return 0; 
}

CF666E Forensic Examination

给定字符串 \(S\)\(m\) 个模式串 \(T_1\sim T_m\)
给定 \(q\) 次询问,每次给定参数 \(p_l,p_r,l,r\),求 \(S\) 的子串 \(S[p_l:p_r]\)\(T_l\sim T_r\) 中哪个串出现次数最多,并输出出现次数。
多解输出下标最小的 \(T\)
\(1\le |S|\le 5 \times 10^5\)\(1\le m,\sum|T_i|\le 5\times 10^4\)\(1\le q\le 5\times 10^5\)
6S,768MB。

多串匹配问题,考虑对 \(T_1\sim T_m\) 建立广义 SAM。
每一个状态都建立一棵线段树,将线段树的节点设为 pair 类型,维护作为 \(S[p_l:p_r]\) 对应状态时的答案。
具体地,在线构建 SAM 时在对应状态的线段树上插入 \(T\) 的下标。线段树合并更新信息。

维护 \(S\) 的每个前缀在 SAM 中对应的状态,可通过将 \(S\) 插入 SAM 中得到。
查询时在 \(S[:p_r]\) 对应状态倍增得到 \(S[p_l:p_r]\) 对应状态。
查询对应状态线段树上 区间 \([l:r]\) 的答案即可。

时间复杂度 \(O(\sum|T_i| + \sum|T_i|\log \sum|T_i| + q\log \sum|T_i|)\) 级别。

代码比较长,注意细节。

//知识点:广义SAM,线段树合并,倍增
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
#define ls (lson[now_])
#define rs (rson[now_])
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 6e5 + 10;
const int kMaxm = 26;
//=============================================================
char S[kMaxn], T[kMaxn];
int node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
int edge_num, head[kMaxn << 1], v[kMaxn << 1], ne[kMaxn << 1];
int dep[kMaxn << 1], fa[kMaxn << 1][22];
int m, pos[kMaxn];
//=============================================================
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;
}
struct SegmentTree {
  int node_num, root[kMaxn << 1], lson[kMaxn << 5], rson[kMaxn << 5];
  pr <int, int> ans[kMaxn << 5];
  void Update(int now_) {
    ans[now_] = std :: max(ans[ls], ans[rs]);
  }
  void Insert(int &now_, int L_, int R_, int pos_) {
    if (! now_) now_ = ++ node_num;
    if (L_ == R_) {
      ++ ans[now_].first;
      ans[now_].second = - L_;
      return ;
    }
    int mid = (L_ + R_) >> 1;
    if (pos_ <= mid) Insert(ls, L_, mid, pos_);
    else Insert(rs, mid + 1, R_, pos_);
    Update(now_);
  }
  int Merge(int x_, int y_, int L_, int R_) {
    if (! x_ || ! y_) return x_ + y_;
    int now_ = ++ node_num;
    if (L_ == R_) {
      ans[now_] = ans[x_];
      ans[now_].first += ans[y_].first;
      return now_;
    }
    int mid = (L_ + R_) >> 1;
    ls = Merge(lson[x_], lson[y_], L_, mid); //傻↑逼↓ (lson写成rson)
    rs = Merge(rson[x_], rson[y_], mid + 1, R_);
    Update(now_);
    return now_;
  }
  pr <int, int> Query(int now_, int L_, int R_, int ql_, int qr_) {
    if (! now_) return pr <int, int> (0, m + 1);
    if (ql_ <= L_ && R_ <= qr_) return ans[now_];
    int mid = (L_ + R_) >> 1;
    pr <int, int> ret (0, m + 1);
    if (ql_ <= mid) ret = std :: max(ret, Query(ls, L_, mid, ql_, qr_));
    if (qr_ > mid) ret = std :: max(ret, Query(rs, mid + 1, R_, ql_, qr_));
    return ret;
  }
} t; 
int Insert(int c_, int last_, int id_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) {
      if (id_) t.Insert(t.root[q], 1, m, id_);
      return q;
    }
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    if (id_) t.Insert(t.root[newq], 1, m, id_);
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    return newq;
  }
  int p = last_, now = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {
    link[now] = 1; 
    if (id_) t.Insert(t.root[now], 1, m, id_);
    return now;
  } 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {
    link[now] = q; 
    if (id_) t.Insert(t.root[now], 1, m, id_);
    return now;
  }
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q])); 
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq;
  if (id_) t.Insert(t.root[now], 1, m, id_);
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  return now;
}
void AddEdge(int u_, int v_) {
  v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
void Dfs(int u_, int fat_) {
  fa[u_][0] = fat_;
  dep[u_] = dep[fat_] + 1;
  for (int i = 1; i <= 20; ++ i) {
    fa[u_][i] = fa[fa[u_][i - 1]][i - 1];
  }
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    Dfs(v_, u_);
    t.root[u_] = t.Merge(t.root[u_], t.root[v_], 1, m);
  }
}
int Get(int x_, int l_) {
  int ret = pos[x_];
  for (int i = 20; i >= 0; -- i) {
    if (fa[ret][i] && len[fa[ret][i]] >= l_) {
      ret = fa[ret][i];
    }
  }
  return ret;
}
void Query(int l_, int r_, int x_, int y_) {
  pr <int, int> ans = t.Query(t.root[Get(y_, y_ - x_ + 1)], 1, m, l_, r_);
  if (! ans.first) ans.second = - l_;
  printf("%d %d\n", - ans.second, ans.first);
}
//=============================================================
int main() {
  scanf("%s", S + 1);
  int n = strlen(S + 1);
  m = read();
  for (int i = 1; i <= m; ++ i) {
    scanf("%s", T + 1);
    int last = 1, nn = strlen(T + 1);
    for (int j = 1; j <= nn; ++ j) last = Insert(T[j] - 'a', last, i);
  }
  int last = 1;
  for (int i = 1; i <= n; ++ i) {
    pos[i] = last = Insert(S[i] - 'a', last, 0);
  }
  for (int i = 2; i <= node_num; ++ i) {
    AddEdge(link[i], i);
  }
  Dfs(1, 0);
  int T = read();
  while (T --) {
    int l = read(), r = read(), x = read(), y = read();
    Query(l, r, x, y);
  }
  return 0; 
}

CF1437G Death DBMS

给定 \(n\) 个字符串,字符串 \(s_i\) 的权值为 \(v_i\)\(v_i\) 初始为 0。
给定 \(m\) 次操作:

  1. 给定参数 \(i,x\),令 \(v_i = x\)
  2. 查询给定字符串 \(q\) 的所有子串的权值的最大值。

\(\sum |s_i|\le 3\times 10^5\)\(\sum |q|\le 3\times 10^5\)
2S,512MB。

这个数据范围长得就很 SAM,考虑对所有字符串建立广义 SAM,记录下每个字符串对应的状态,再对 parent 树进行重链剖分。
对于修改操作,单点修改权值即可,复杂度 \(O(\log n)\)
对于查询操作,类似这个题的套路 P4081,边跑前缀状态,边在线段树上查到根的点权的最大值即可,复杂度 \(O(q\log^2 n)\)

因为连续前缀对应状态在 SAM 上也是连续的,把串扔到 SAM 上跑 即得所有前缀对应状态。
前缀对应状态到 parent 树根的链上 包含该前缀所有后缀,可以包含所有子串信息。

总复杂度 \(O(L \log^2 L)\),其中 \(L\) 是所有字符串的长度总和。可以用 LCT 做到常数较大的一个 \(\log\)

注意可能出现重复名字,对 SAM 的每一个节点额外维护一个 multiset 即可。

代码先不写了,可以看这位神仙的:题解 CF1437G 【Death DBMS】 - lory1608 的博客

写在最后

参考资料:

《后缀自动机在字典树上的拓展》 - 刘研绎
【学习笔记】字符串—广义后缀自动机 - 辰星凌

posted @ 2021-01-06 15:58  Luckyblock  阅读(389)  评论(0编辑  收藏  举报