AC 自动机
AC 自动机
ACAM 通常用于解决多模式匹配问题:给定若干模式串 \(T_{1 \sim m}\) 与文本串 \(S\) ,需要统计 \(T_i\) 在 \(S\) 的出现情况。
ACAM 的结构融合了:
- Trie 结构:对所有的模式串建一棵 Trie 。
- KMP 思想:对 Trie 树上所有的结点构建失配指针。
实现
构建 fail 树
先建立模式串的 Trie 树,定义状态 \(u\) 的 fail 指针指向其最长真后缀。
考虑 bfs 求解,设当前求解的点为 \(u\) ,其父亲为 \(f\) ,\(f\) 通过字符 \(c\) 的边指向 \(u\) :
- 若 \(fail_f\) 通过 \(c\) 连接到的子节点 \(w\) 存在,则令 \(fail_u \leftarrow w\) 。
- 否则继续找 \(fail_{fail_f}\) ,重复上一步直到跳到根为止。
- 若跳到根,则说明其没有真后缀,令 \(fail_u \leftarrow rt\) 。
发现第二步最坏是单次 \(O(n)\) 的,考虑路径压缩优化:若 \(u\) 没有字符 \(c\) 的出边,则令 \(tr(u, c) \gets tr(fail_u, c)\) 即可。
时间复杂度 \(O(\sum |S_i| \times |\sum|)\) 。
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
}
当字符集很大的时候,可以发现 ch[u]
实际就是先继承 ch[fail[u]]
然后做若干单点修改。因此考虑用 map
存储 ch
,用主席树存储路径压缩后的 ch
,时间复杂度 \(O(\sum |S_i| \times \log |\sum|)\) 。
namespace ACAM {
map<int, int> ch[N];
int fail[N];
int tot = 1;
inline int insert(vector<int> &vec) {
int u = 1;
for (int it : vec) {
if (ch[u].find(it) == ch[u].end())
ch[u][it] = ++tot;
u = ch[u][it];
}
return u;
}
namespace SMT {
const int S = 5e6 + 7;
int rt[N], lc[S], rc[S], val[S];
int tot;
int update(int x, int nl, int nr, int p, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x];
if (nl == nr)
return val[y] = k, y;
int mid = (nl + nr) >> 1;
if (p <= mid)
lc[y] = update(lc[x], nl, mid, p, k);
else
rc[y] = update(rc[x], mid + 1, nr, p, k);
return y;
}
int query(int x, int nl, int nr, int p) {
if (!x)
return 1;
if (nl == nr)
return val[x];
int mid = (nl + nr) >> 1;
return p <= mid ? query(lc[x], nl, mid, p) : query(rc[x], mid + 1, nr, p);
}
} // namespace SMT
inline void build() {
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), SMT::rt[u] = SMT::rt[fail[u]];
for (auto it : ch[u]) {
SMT::rt[u] = SMT::update(SMT::rt[u], 0, S, it.first, it.second);
fail[it.second] = SMT::query(SMT::rt[fail[u]], 0, S, it.first);
q.emplace(it.second);
}
}
}
} // namespace ACAM
如果对空间要求比较严苛,并且后续不会用到 ch
数组,也可以采用线段树合并。
文本串匹配
考虑从 \(S[1, i - 1]\) 的匹配状态转移到 \(S[1, i]\) 的匹配状态,不难发现只要不断跳 fail 指针,直到当前状态有 \(S[i]\) 的转移即可。
事实上在 build
函数中 ch
已经路径压缩为跳 fail
之后的状态,因此直接跳 ch
即可。
跑到状态 \(u\) 时,\(u\) 在 fail 树上到根的链上所有的终止节点都是其后缀,都会产生匹配。
问题转化为 fail 树上的链上求和,树上差分转化为单点加-子树求和。
如果是单次查询,只要最后做一遍子树求和即可,时间复杂度 \(O(|S| + \sum |T_i|)\) 。
如果是多次查询,需要用树状数组维护,时间复杂度 \(O(|S| \log (\sum |T_i|))\) 。
void dfs(int u) {
for (int v : G.e[u])
dfs(v), ans[u] += ans[v];
}
inline void query(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i)
++ans[u = ch[u][str[i] - 'a']];
dfs(1);
}
应用
fail 树上的信息统计
CF1202E You Are Given Some Strings...
给定 \(n\) 个串 \(S_{1 \sim n}\) 与一个串 \(T\) ,设 \(f(t, s)\) 表示 \(s\) 在 \(t\) 中的出现次数,求:
\[\sum_{i = 1}^n \sum_{j = 1}^n f(T, S_i + S_j) \]\(n, \sum |S_i|, |T| \le 2 \times 10^5\)
枚举 \(T\) 的前缀 \(T[1, k]\) ,则只要将作为 \(T[1, k]\) 后缀的 \(S\) 的数量与作为 \(T[k + 1, |T|]\) 前缀的 \(S\) 的数量相乘即可。
考虑对 \(S_{1 \sim n}\) 建立正串、反串分别建出 ACAM,则两个信息不难表示为 fail 树上的链求和。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, S = 26;
int id[N], ida[N], idb[N];
char str[N], t[N];
int n;
struct ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fail[N], cnt[N];
int tot = 1;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
++cnt[u];
}
void dfs(int u) {
for (int v : G.e[u])
cnt[v] += cnt[u], dfs(v);
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
dfs(1);
}
} A, B;
signed main() {
scanf("%s%d", str + 1, &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", t + 1);
int len = strlen(t + 1);
A.insert(t, len), reverse(t + 1, t + len + 1), B.insert(t, len);
}
A.build(), B.build(), n = strlen(str + 1);
ida[0] = 1;
for (int i = 1; i <= n; ++i)
ida[i] = A.ch[ida[i - 1]][str[i] - 'a'];
idb[n + 1] = 1;
for (int i = n; i; --i)
idb[i] = B.ch[idb[i + 1]][str[i] - 'a'];
ll ans = 0;
for (int i = 1; i <= n; ++i)
ans += 1ll * A.cnt[ida[i]] * B.cnt[idb[i + 1]];
printf("%lld", ans);
return 0;
}
CF163E e-Government
给定 \(k\) 个字符串集合 \(S_{1 \sim k}\) ,\(n\) 次操作,操作有:
- 询问集合内所有串在询问串中的出现次数总和。
- 向集合中加入 \(S_i\) 。
- 从集合中删除 \(S_i\) 。
\(k, n \le 10^5\) ,字符串总长 \(\le 10^6\)
先不考虑集合的变动,则问题转化为若干次链查询,可以预处理每个点到根的终止节点数量做到线性。
接下来考虑集合变动,发现一次修改只会改变一个子树内“点到根的终止节点数量“,直接树状数组维护即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 7, S = 26;
int id[N];
char str[N];
bool exist[N];
int n, m;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fail[N], in[N], out[N];
int tot = 1, dfstime;
inline int insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
return u;
}
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs(v);
out[u] = dfstime;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
dfs(1);
}
namespace BIT {
int c[N];
inline void modify(int x, int k) {
for (; x <= tot; x += x & -x)
c[x] += k;
}
inline void update(int l, int r, int k) {
modify(l, k), modify(r + 1, -k);
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
} // namespace BIT
inline ll query(char *str, int n) {
ll ans = 0;
for (int i = 1, u = 1; i <= n; ++i)
ans += BIT::query(in[u = ch[u][str[i] - 'a']]);
return ans;
}
} // namespace ACAM
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> m >> n;
for (int i = 1; i <= n; ++i) {
cin >> (str + 1);
id[i] = ACAM::insert(str, strlen(str + 1));
}
ACAM::build();
for (int i = 1; i <= n; ++i)
ACAM::BIT::update(ACAM::in[id[i]], ACAM::out[id[i]], 1), exist[i] = true;
while (m--) {
char op;
cin >> op;
if (op == '?') {
cin >> (str + 1);
printf("%lld\n", ACAM::query(str, strlen(str + 1)));
} else {
int x;
cin >> x;
if (op == '+' && !exist[x])
ACAM::BIT::update(ACAM::in[id[x]], ACAM::out[id[x]], 1), exist[x] = true;
else if (op == '-' && exist[x])
ACAM::BIT::update(ACAM::in[id[x]], ACAM::out[id[x]], -1), exist[x] = false;
}
}
return 0;
}
CF547E Mike and Friends
给定 \(n\) 个字符串 \(S_{1 \sim n}\) ,\(q\) 次询问 \(S_x\) 在 \(S_{l \sim r}\) 中的出现次数。
\(n, \sum |S_i| \le 2 \times 10^5\) ,\(q \le 5 \times 10^5\)
离线将询问拆为 \(S_x\) 在 \(S_{1 \sim y}\) 中的出现次数。对 \(y\) 扫描线,不断对前缀节点标记 \(+1\) ,出现次数即为链查询,树上差分即可转化为子树查询。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, Q = 5e5 + 7, S = 26;
vector<pair<int, int> > qry[N];
int id[N], ans[Q];
char str[N];
int n, q;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fa[N], fail[N], in[N], out[N];
int tot = 1, dfstime;
inline int insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
fa[ch[u][c] = ++tot] = u;
u = ch[u][c];
}
return u;
}
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs(v);
out[u] = dfstime;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
dfs(1);
}
} // namespace ACAM
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= ACAM::tot; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} // namespace BIT
signed main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i)
scanf("%s", str + 1), id[i] = ACAM::insert(str, strlen(str + 1));
ACAM::build();
for (int i = 1; i <= q; ++i) {
int l, r, x;
scanf("%d%d%d", &l, &r, &x);
qry[r].emplace_back(x, i), qry[l - 1].emplace_back(x, -i);
}
for (int i = 1; i <= n; ++i) {
for (int x = id[i]; x; x = ACAM::fa[x])
BIT::update(ACAM::in[x], 1);
for (auto it : qry[i]) {
if (it.second > 0)
ans[it.second] += BIT::query(ACAM::in[id[it.first]], ACAM::out[id[it.first]]);
else
ans[-it.second] -= BIT::query(ACAM::in[id[it.first]], ACAM::out[id[it.first]]);
}
}
for (int i = 1; i <= q; ++i)
printf("%d\n", ans[i]);
return 0;
}
P2336 [SCOI2012] 喵星球上的点名
有 \(n\) 只喵,每只喵有一个名和一个姓(两个字符串),还有 \(m\) 次点名(也是一个字符串),如果一只喵的名或姓中包含这个字符串,这只喵就会喊到。求:
- 对于每次点名询问有多少只喵喊到。
- 对于每一只喵问询它喊了多少次到。
\(|\sum| \le 10^4\) ,\(\sum |S| \le 2 \times 10^5\)
把一只喵的名和姓合并在一起,中间插入一个无关字符,与询问串一起建立 AC 自动机。
- 第一问:对于一只喵,所有它会喊到的状态即为其所有前缀状态到根的链的并,不难树上差分解决。
- 第二问:与第一问类似,一个询问串产生的贡献为一个子树,树上差分转化为单点修改与链求并。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, S = 1e4 + 7, LOGN = 19;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int fa[N][LOGN], nameid[N], qryid[N], dep[N], siz[N], dfn[N], num[N];
int n, m, dfstime;
namespace ACAM {
map<int, int> ch[N];
int fa[N], fail[N];
int tot = 1;
inline int insert(vector<int> &vec) {
int u = 1;
for (int it : vec) {
if (ch[u].find(it) == ch[u].end())
fa[ch[u][it] = ++tot] = u;
u = ch[u][it];
}
return u;
}
namespace SMT {
const int S = 5e6 + 7;
int rt[N], lc[S], rc[S], val[S];
int tot;
int update(int x, int nl, int nr, int p, int k) {
int y = ++tot;
lc[y] = lc[x], rc[y] = rc[x];
if (nl == nr)
return val[y] = k, y;
int mid = (nl + nr) >> 1;
if (p <= mid)
lc[y] = update(lc[x], nl, mid, p, k);
else
rc[y] = update(rc[x], mid + 1, nr, p, k);
return y;
}
int query(int x, int nl, int nr, int p) {
if (!x)
return 1;
if (nl == nr)
return val[x];
int mid = (nl + nr) >> 1;
return p <= mid ? query(lc[x], nl, mid, p) : query(rc[x], mid + 1, nr, p);
}
} // namespace SMT
inline void build() {
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), SMT::rt[u] = SMT::rt[fail[u]];
for (auto it : ch[u]) {
SMT::rt[u] = SMT::update(SMT::rt[u], 0, S, it.first, it.second);
fail[it.second] = SMT::query(SMT::rt[fail[u]], 0, S, it.first);
q.emplace(it.second);
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
} // namespace ACAM
void dfs1(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1, siz[u] = 1, dfn[u] = ++dfstime;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
dfs1(v, u), siz[u] += siz[v];
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
void dfs2(int u) {
for (int v : G.e[u])
dfs2(v), num[u] += num[v];
}
void dfs3(int u) {
num[u] += num[fa[u][0]];
for (int v : G.e[u])
dfs3(v);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
int len;
scanf("%d", &len);
vector<int> vec(len);
for (int &it : vec)
scanf("%d", &it);
vec.emplace_back(S);
scanf("%d", &len);
while (len--) {
int x;
scanf("%d", &x);
vec.emplace_back(x);
}
nameid[i] = ACAM::insert(vec);
}
for (int i = 1; i <= m; ++i) {
int len;
scanf("%d", &len);
vector<int> vec(len);
for (int &it : vec)
scanf("%d", &it);
qryid[i] = ACAM::insert(vec);
}
ACAM::build(), dfs1(1, 0);
for (int i = 1; i <= n; ++i) {
vector<int> kp;
for (int u = nameid[i]; u; u = ACAM::fa[u])
kp.emplace_back(u), ++num[u];
sort(kp.begin(), kp.end(), [](const int &a, const int &b) {
return dfn[a] < dfn[b];
});
for (int i = 1; i < kp.size(); ++i)
--num[LCA(kp[i - 1], kp[i])];
}
dfs2(1);
for (int i = 1; i <= m; ++i)
printf("%d\n", num[qryid[i]]);
memset(num + 1, 0, sizeof(int) * ACAM::tot);
for (int i = 1; i <= m; ++i)
++num[qryid[i]];
dfs3(1);
for (int i = 1; i <= n; ++i) {
vector<int> kp;
int res = 0;
for (int u = nameid[i]; u; u = ACAM::fa[u])
kp.emplace_back(u), res += num[u];
sort(kp.begin(), kp.end(), [](const int &a, const int &b) {
return dfn[a] < dfn[b];
});
for (int i = 1; i < kp.size(); ++i)
res -= num[LCA(kp[i - 1], kp[i])];
printf("%d ", res);
}
return 0;
}
CF1483F Exam
给定 \(n\) 个串 \(S_{1 \sim n}\) ,求有多少对 \((i, j)\) 满足:
- \(i \ne j\) 。
- \(S_j\) 是 \(S_i\) 的子串。
- 不存在 \(k \ (k \ne i, k \ne j)\) 满足 \(S_j\) 是 \(S_k\) 的子串且 \(S_k\) 是 \(S_i\) 的子串。
\(\sum |S_i| \le 10^6\)
考虑枚举 \(i\) ,计算 \(j\) 的数量。
考虑 \(S_i\) 的每个前缀 \(S_i[1, k]\) ,一个后缀合法当且仅当其是 \(S_{1 \sim n}\) 之一,且左端点要小于后面所有 \(k\) 对应的左端点,否则就会被包含。
但是直接算会有问题,可能会出现某个串在一个位置合法,但是在另一个位置不合法的情况。此时只要判一下出现次数与完全合法次数是否相等即可,出现次数直接做一个子树求和就行了。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, S = 26;
int len[N], lpos[N], match[N], cnt[N];
char _str[N], *str[N];
int n;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fail[N], anc[N], len[N], in[N], out[N];
int tot = 1, dfstime;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
anc[u] = u, len[u] = n;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u]) {
if (!anc[v])
anc[v] = anc[u];
dfs(v);
}
out[u] = dfstime;
}
} // namespace ACAM
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= ACAM::tot; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} // namespace BIT
signed main() {
scanf("%d", &n);
str[0] = _str;
for (int i = 1; i <= n; ++i) {
str[i] = str[i - 1] + len[i - 1];
scanf("%s", str[i] + 1);
ACAM::insert(str[i], len[i] = strlen(str[i] + 1));
}
ACAM::build(), ACAM::dfs(1);
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1, u = 1; j <= len[i]; ++j) {
u = ACAM::ch[u][str[i][j] - 'a'], BIT::update(ACAM::in[u], 1);
if (j == len[i])
u = ACAM::fail[u];
lpos[j] = j - ACAM::len[ACAM::anc[u]] + 1, match[j] = ACAM::anc[u];
}
vector<int> kp;
for (int j = len[i], p = len[i]; j; p = min(p, lpos[j--] - 1))
if (lpos[j] <= min(j, p))
kp.emplace_back(match[j]), ++cnt[match[j]];
for (int it : kp) {
if (!cnt[it])
continue;
if (BIT::query(ACAM::in[it], ACAM::out[it]) == cnt[it])
++ans;
cnt[it] = 0;
}
for (int j = 1, u = 1; j <= len[i]; ++j)
u = ACAM::ch[u][str[i][j] - 'a'], BIT::update(ACAM::in[u], -1);
}
printf("%d", ans);
return 0;
}
P3735 [HAOI2017] 字符串
给出一个字符串 \(S\) 和 \(n\) 个字符串 \(p_i\),求每个字符串 \(p_i\) 在 \(S\) 中出现的次数。
这里两个字符串相等的定义稍作改变:给定一个常数 \(k\) ,对于两个字符串 \(a, b\),\(a = b\) 当且仅当:
- \(|a| = |b|\) 。
- \(|a| = |b| \le k\) 或 \(\forall i, j, a_i \ne b_i, a_j \ne b_j, |i - j| < k\) 。
\(|s|, \sum |p_i| \le 2 \times 10^5\)
不难发现相等的条件即为 \(\mathrm{LCP}(a, b) + \mathrm{LCS}(a, b) + k \ge |a|\) ,考虑固定 LCP 求合法的 LCS 数量。
对所有 \(p_i\) 的正反串一起建立 AC 自动机,对于前缀 \(p_i[1, j]\) ,将 \(j + k + 1\) 后缀对应点挂在 \(j\) 对应前缀点上。
对于给出的文本串 \(S\) 的前缀 \(S[1, i]\) ,将 \(i + k + 1\) 后缀对应点挂在 \(i\) 对应前缀点上。
遍历一遍 fail 树,考虑将限制条件表示在 fail 树上。对于一对 \(S[1, i] - S[i + k + 1, n]\) 的关系,则 \(S[1, i]\) 应当在 \(p[1, j]\) 的子树内,且 \(S[i + k + 1, n]\) 也应当在 \(p[j + k + 1, |p|]\) 的子树内。用树状数组维护,前者可以在进入子树前后的贡献做差处理,后者直接区间查询即可。
但是直接这样做会算重,当 \(lcp + lcs + k > |s|\) 时,一个匹配的位置会计算多次。考虑钦定此时令 LCS 取到最大值才统计贡献,可以通过在节点上再挂上 \(j + k\) 位置的后缀对应的节点减去其贡献做到。
需要注意当 LCP 为空的时候不用减去贡献,因为此时 LCP 一定最小(空)。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, S = 127;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int id, p1, p2;
};
vector<Node> qry[N];
vector<pair<int, int> > upd[N];
int ans[N], in[N], out[N];
char str[N], p[N];
int k, m, n, dfstime;
namespace ACAM {
int ch[N][S], suf[N], fail[N];
int tot = 1;
inline void insert(char *str, int n, int id) {
for (int i = 1, u = 1; i <= n; ++i) {
if (!ch[u][str[i]])
ch[u][str[i]] = ++tot;
u = ch[u][str[i]];
}
suf[n + 1] = 1;
for (int i = n, u = 1; i; --i) {
if (!ch[u][str[i]])
ch[u][str[i]] = ++tot;
suf[i] = u = ch[u][str[i]];
}
for (int i = 0, u = 1; i <= n - k; u = ch[u][str[++i]])
qry[u].emplace_back((Node){id, suf[i + k + 1], i ? suf[i + k] : -1});
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
suf[n + 1] = 1;
for (int i = n; i; --i)
suf[i] = ch[suf[i + 1]][str[i]];
for (int i = 0, u = 1; i <= n - k; u = ch[u][str[++i]])
upd[u].emplace_back(suf[i + k + 1], suf[i + k]);
}
} // namespace ACAM
void dfs1(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs1(v);
out[u] = dfstime;
}
struct BIT {
int c[N];
int n;
inline void prework(int _n) {
memset(c + 1, 0, sizeof(int) * (n = _n));
}
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} bit1, bit2;
void dfs2(int u) {
for (Node it : qry[u]) {
ans[it.id] -= bit1.query(in[it.p1], out[it.p1]);
if (~it.p2)
ans[it.id] -= bit2.query(in[it.p2], out[it.p2]);
}
for (auto it : upd[u])
bit1.update(in[it.first], 1), bit2.update(in[it.second], -1);
for (int v : G.e[u])
dfs2(v);
for (Node it : qry[u]) {
ans[it.id] += bit1.query(in[it.p1], out[it.p1]);
if (~it.p2)
ans[it.id] += bit2.query(in[it.p2], out[it.p2]);
}
}
signed main() {
scanf("%d%s%d", &k, str + 1, &m);
n = strlen(str + 1);
for (int i = 1; i <= m; ++i) {
scanf("%s", p + 1);
int len = strlen(p + 1);
if (len <= k)
ans[i] = n - len + 1;
else
ACAM::insert(p, len, i);
}
ACAM::build(), dfs1(1);
bit1.prework(ACAM::tot), bit2.prework(ACAM::tot);
dfs2(1);
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
P2414 [NOI2011] 阿狸的打字机
给出一个包含小写字符和
B
、P
的字符串,分别表示:
- 小写字符:在当前串末尾插入该字符。
B
:删除当前串末尾字符。P
:打印当前串。\(m\) 次询问,每个询问第 \(x\) 次打印的字符串在第 \(y\) 次打印的字符串中的出现次数。
\(n, m \le 10^5\)
不难发现三种操作都可以在 Trie 树上单次 \(O(1)\) 完成,于是可以线性构建出 Trie 树,询问可以转化为 \(S_y\) 在 Trie 树上的祖先链中属于 \(S_x\) 在 fail 树上子树的点数。
考虑离线,对于询问 \((x, y)\) ,将其放在 \(y\) 上。最后遍历一遍 Trie,则只要查询 \(x\) 子树内有多少 \(y\) 的前缀节点即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
int ans[N], in[N], out[N];
char str[N];
int n, m, dfstime;
namespace ACAM {
int ch[N][26], fa[N], fail[N], id[N];
int tot = 1, cnt;
inline void prework(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
if (str[i] == 'B')
u = fa[u];
else if (str[i] == 'P')
id[++cnt] = u;
else {
int c = str[i] - 'a';
if (!ch[u][c])
fa[ch[u][c] = ++tot] = u;
u = ch[u][c];
}
}
}
inline void build() {
fill(ch[0], ch[0] + 26, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
}
} // namespace ACAM
void dfs(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs(v);
out[u] = dfstime;
}
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= ACAM::tot; x += x & -x)
c[x] += k;
}
inline int ask(int x) {
int res = 0;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query(int l, int r) {
return ask(r) - ask(l - 1);
}
} // namespace BIT
signed main() {
scanf("%s%d", str + 1, &m), n = strlen(str + 1);
ACAM::prework(str, n), ACAM::build(), dfs(1);
for (int i = 1, x, y; i <= m; ++i) {
scanf("%d%d", &x, &y);
qry[y].emplace_back(x, i);
}
for (int i = 1, u = 1, id = 0; i <= n; ++i) {
if (str[i] == 'B')
BIT::update(in[u], -1), u = ACAM::fa[u];
else if (str[i] == 'P') {
++id;
for (auto it : qry[id])
ans[it.second] = BIT::query(in[ACAM::id[it.first]], out[ACAM::id[it.first]]);
} else
BIT::update(in[u = ACAM::ch[u][str[i] - 'a']], 1);
}
for (int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
CF587F Duff is Mad
给定 \(n\) 个字符串 \(S_{1 \sim n}\) ,\(q\) 次询问 \(S_{l \sim r}\) 在 \(S_x\) 中的出现次数和。
\(n, q, \sum |S_i| \le 10^5\)
记 \(m = \sum |S_i|\) ,对 \(|S_x|\) 根号分治:
- \(|S_x| > B\) :不难发现这样的 \(x\) 只有 \(O(\frac{m}{B})\) 个。考虑将 \(S_x\) 相同的串一起处理,给 \(S_x\) 所有前缀对应状态打上标记,预处理出一个状态的贡献(子树和),则只要查询 \(S_{l \sim r}\) 状态的贡献和即可,预处理前缀和即可做到 \(O(\frac{m}{B} \times m)\) 。
- \(|S_x| \le B\) :此时一组询问可以 \(O(|S_x|)\) 处理。先将询问拆为 \((1, r, x) - (1, l - 1, x)\) ,将其挂在 \(r\) 和 \(l - 1\) 上。离线按顺序加入每个前缀点,问题转化为 \(O(|S_x|)\) 次链查询。树上差分为 \(O(n)\) 次子树加和 \(O(qB)\) 次单点查询,用 \(O(\sqrt{m} - O(1))\) 的块状数组维护即可做到 \(O(n \sqrt{m} + qB)\) 。
取 \(B = \frac{m}{\sqrt{q}}\) ,时间复杂度 \(O(n \sqrt{m} + m \sqrt{q})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, S = 26;
vector<tuple<int, int, int> > qry1[N];
vector<pair<int, int> > qry2[N];
string str[N];
ll ans[N], s[N];
int id[N];
int n, m, q;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int ch[N][S], fail[N], in[N], out[N], cnt[N];
int tot = 1, dfstime;
inline int insert(string str) {
int u = 1;
for (int i = 0; i < str.length(); ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
return u;
}
void dfs1(int u) {
in[u] = ++dfstime;
for (int v : G.e[u])
dfs1(v);
out[u] = dfstime;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
dfs1(1);
}
void dfs2(int u) {
for (int v : G.e[u])
dfs2(v), cnt[u] += cnt[v];
}
inline void prework(string str) {
memset(cnt + 1, 0, sizeof(int) * tot);
for (int i = 0, u = 1; i < str.length(); ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
++cnt[u = ch[u][c]];
}
dfs2(1);
}
namespace FK {
int tag[N], val[N];
int block;
inline void update(int l, int r, int k) {
int x = (l - 1) / block + 1, y = (r - 1) / block + 1;
if (x == y) {
for (int i = l; i <= r; ++i)
val[i] += k;
} else {
for (int i = l; i <= x * block; ++i)
val[i] += k;
for (int i = x + 1; i < y; ++i)
tag[i] += k;
for (int i = (y - 1) * block + 1; i <= r; ++i)
val[i] += k;
}
}
inline int query(int x) {
return val[x] + tag[(x - 1) / block + 1];
}
} // namespace FK
inline ll query(string str) {
ll res = 0;
for (int i = 0, u = 1; i < str.length(); ++i)
res += FK::query(in[u = ch[u][str[i] - 'a']]);
return res;
}
} // namespace ACAM
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> q;
for (int i = 1; i <= n; ++i)
cin >> str[i], id[i] = ACAM::insert(str[i]), m += str[i].length();
ACAM::build();
int block = m / sqrt(q);
for (int i = 1; i <= q; ++i) {
int l, r, x;
cin >> l >> r >> x;
if (str[x].length() > block)
qry1[x].emplace_back(l, r, i);
else
qry2[r].emplace_back(x, i), qry2[l - 1].emplace_back(x, -i);
}
for (int i = 1; i <= n; ++i) {
if (qry1[i].empty())
continue;
ACAM::prework(str[i]);
for (int j = 1; j <= n; ++j)
s[j] = s[j - 1] + ACAM::cnt[id[j]];
for (auto it : qry1[i])
ans[get<2>(it)] = s[get<1>(it)] - s[get<0>(it) - 1];
}
ACAM::FK::block = sqrt(m);
for (int i = 1; i <= n; ++i) {
ACAM::FK::update(ACAM::in[id[i]], ACAM::out[id[i]], 1);
for (auto it : qry2[i]) {
if (it.second > 0)
ans[it.second] += ACAM::query(str[it.first]);
else
ans[-it.second] -= ACAM::query(str[it.first]);
}
}
for (int i = 1; i <= q; ++i)
printf("%lld\n", ans[i]);
return 0;
}
LOJ6681. yww 与树上的回文串
给一棵树,每条边上有 \(0\) 或 \(1\) 的权值,求有多少对 \((x, y)\) 满足 \(x < y\) 且 \(x\) 到 \(y\) 路径上的边上的字符按顺序组成的字符串为回文串。
\(n \le 50000\)。
树上路径信息统计首选点分治,每次计算经过重心的串的贡献。
观察一个经过重心的回文串,其可以被划分为 \(ST|S\) ,其中 \(|\) 表示重心,\(T\) 为一个回文串。
考虑枚举 \(ST\) , 对另一侧的 \(S\) 建出 ACAM ,便于统计匹配。
由于 \(T\) 是一个串的回文前缀,根据回文 border 理论,其可以被划分为 \(O(\log n)\) 个等差序列,问题转化为对某个串询问其长度为一个等差序列的后缀的匹配次数总和。
根据 ACAM 的经典理论,若求长度为 \(k\) 的后缀的匹配次数总和,只要求 fail 树的长度为 \(k\) 的祖先串的贡献即可。
接下来考虑根号分治。
-
对于公差 \(\le \sqrt{n}\) 的的等差序列,在 fail 树上预处理模分类树上前缀和。记当前等差序列为 \((l, r, d)\) ,对长度为 \([l, r]\) 的祖先查询长度模 \(d\) 同余的出现次数即可,不难预处理前缀和做到 \(O(n \sqrt{n})\) 。
-
对于公差 \(> \sqrt{n}\) 的等差序列,元素个数总和 \(O(\sum_{k = 1} \frac{\sqrt{n}}{2^k}) = O(\sqrt{n})\) ,暴力即可。
单次处理复杂度为 \(O(n \sqrt{n})\) ,总时间复杂度为 \(T(n) = 2 T(\frac{n}{2}) + O(n \sqrt{n}) = O(n \sqrt{n})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e4 + 7, B = 227, S = 2;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
int pw[N], siz[N], mxsiz[N];
bool vis[N];
ll ans;
int n, root;
namespace ACAM {
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 0; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct Node {
int l, r, d;
};
vector<Node> vec[N], qry[N];
vector<pair<int, int> > sta = {make_pair(-1, -1)};
int ch[N][S], sum[B][B];
int fail[N], cnt[N], len[N], edg[N], sh[N], buc[N];
int tot, block;
inline void prework() {
memset(ch + 1, 0, sizeof(int) * S * (tot + 1));
memset(fail + 1, 0, sizeof(int) * (tot + 1));
memset(cnt + 1, 0, sizeof(int) * (tot + 1));
for (int i = 1; i <= tot; ++i)
vec[i].clear(), qry[i].clear();
tot = 1;
}
void dfs1(int u, int hs) {
if (len[u] && hs == sh[len[u] / 2]) {
if (vec[u].empty())
vec[u].emplace_back((Node) {len[u], len[u], -1});
else {
Node cur = vec[u].back();
if (cur.d == -1)
vec[u].back() = (Node) {cur.l, len[u], len[u] - cur.l};
else if (len[u] == cur.r + cur.d)
vec[u].back().r = len[u];
else
vec[u].emplace_back((Node) {len[u], len[u], -1});
}
}
for (int i = 0; i < S; ++i) {
if (!ch[u][i])
continue;
int v = ch[u][i];
vec[v] = vec[u], len[v] = len[u] + 1, edg[len[v]] = i;
sh[len[v]] = (sh[len[u]] + 1ll * pw[len[u]] * i) % Mod;
dfs1(v, ((hs << 1 | i) % Mod - 1ll * (len[v] & 1) * edg[len[u] / 2 + 1] * pw[len[u] / 2] % Mod + Mod) % Mod);
}
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i)
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
G.clear(tot);
for (int i = 1; i <= tot; ++i)
G.insert(fail[i], i);
}
ll dfs2(int u) {
sta.emplace_back(len[u], u), buc[len[u]] = cnt[u];
ll res = 1ll * cnt[u] * (cnt[u] - 1) / 2;
for (Node it : vec[u]) {
int l = len[u] - it.r, r = len[u] - it.l, d = it.d;
if (l == r)
res += 1ll * buc[l] * cnt[u];
else if (d > block) {
for (int i = l; i <= r; i += d)
res += 1ll * buc[i] * cnt[u];
} else {
int nl = prev(lower_bound(sta.begin() + 1, sta.end(), make_pair(l, 0)))->second,
nr = prev(lower_bound(sta.begin() + 1, sta.end(), make_pair(r + 1, 0)))->second;
if (nl != nr) {
if (~nl)
qry[nl].emplace_back((Node) {d, l % d, -cnt[u]});
if (~nr)
qry[nr].emplace_back((Node) {d, l % d, cnt[u]});
}
}
}
for (int v : G.e[u])
res += dfs2(v);
buc[len[u]] = 0, sta.pop_back();
return res;
}
ll dfs3(int u) {
for (int i = 1; i <= block; ++i)
sum[i][len[u] % i] += cnt[u];
ll res = 0;
for (Node it : qry[u])
res += 1ll * it.d * sum[it.l][it.r];
for (int v : G.e[u])
res += dfs3(v);
for (int i = 1; i <= block; ++i)
sum[i][len[u] % i] -= cnt[u];
return res;
}
inline ll solve() {
dfs1(1, 0), build();
return dfs2(1) + dfs3(1);
}
} // namespace ACAM
int getsiz(int u, int f) {
siz[u] = 1;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
}
return siz[u];
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
}
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f, int x) {
++ACAM::cnt[x];
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!vis[v] && v != f) {
if (!ACAM::ch[x][w])
ACAM::ch[x][w] = ++ACAM::tot;
dfs(v, u, ACAM::ch[x][w]);
}
}
}
void solve(int u) {
vis[u] = true, ACAM::block = sqrt(getsiz(u, 0));
ACAM::prework(), dfs(u, 0, 1), ans += ACAM::solve();
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (!vis[v])
ACAM::prework(), dfs(v, u, ACAM::ch[1][w] = ++ACAM::tot), ans -= ACAM::solve();
}
for (auto it : G.e[u]) {
int v = it.first;
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), solve(root);
}
}
signed main() {
scanf("%d", &n);
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
for (int i = 1; i < n; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
root = 0, getroot(1, 0, n), solve(root);
printf("%lld", ans);
return 0;
}
CF1801G A task for substrings
给定字符串 \(T\) 与 \(n\) 个单词 \(S_{1 \sim n}\) ,\(m\) 次询问 \(T[l, r]\) 中 \(S_{1 \sim n}\) 的出现次数和。
\(n, m \le 5 \times 10^5\) ,\(\sum |S_i| \le 10^6\) ,\(|T| \le 5 \times 10^6\)
设 \(f_i\) 表示 \(T[1, i]\) 有多少个后缀为单词,\(g_i\) 表示 \(T[1, i]\) 最长为其后缀的单词编号,\(s_i = \sum_{j \le i} f_i\) ,不难对正串建出 ACAM 求出。
对于一次询问,线段树二分找到最大的 \(p\) 满足 \(p - |S_{g_p}| < l\) ,则 \(q + 1 \sim r\) 的答案即为 \(s_r - s_p\) ,\(l \sim p\) 的答案即为 \(S_{g_p}\) 中长度为 \(p - l + 1\) 的后缀中单词的出现次数,不难对反串建立 ACAM 求出。
时间复杂度 \(O(\sum |S_i| + |T| + m \log |T|)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7, M = 5e6 + 7, L = 1e6 + 7, S = 26;
struct ACAM {
struct Graph {
vector<int> e[L];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
pair<int, int> mxlen[L];
int ch[L][S], fail[L], cnt[L];
int tot = 1;
inline void insert(string str, int id) {
int u = 1;
for (char c : str) {
if (!ch[u][c - 'a'])
ch[u][c - 'a'] = ++tot;
u = ch[u][c - 'a'];
}
++cnt[u], mxlen[u] = max(mxlen[u], make_pair((int)str.length(), id));
}
void dfs(int u) {
for (int v : G.e[u])
cnt[v] += cnt[u], mxlen[v] = max(mxlen[v], mxlen[u]), dfs(v);
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 2; i <= tot; ++i)
G.insert(fail[i], i);
dfs(1);
}
} A, B;
string t[N];
vector<int> ans[N];
ll s[M];
int f[M], g[M];
char str[M];
int n, q, m;
namespace SMT {
int mn[M << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void build(int x, int l, int r) {
if (l == r) {
mn[x] = l - t[g[l]].length();
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
int query(int x, int nl, int nr, int l, int r) {
if (r < nl || l > nr || mn[x] >= l)
return -1;
else if (nl == nr)
return nl;
int mid = (nl + nr) >> 1, res = query(rs(x), mid + 1, nr, l, r);
return ~res ? res : query(ls(x), nl, mid, l, r);
}
} // namespace SMT
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> q >> (str + 1), m = strlen(str + 1);
for (int i = 1; i <= n; ++i) {
cin >> t[i];
A.insert(t[i], i), reverse(t[i].begin(), t[i].end()), B.insert(t[i], i);
}
A.build(), B.build();
for (int i = 1, u = 1; i <= m; ++i)
u = A.ch[u][str[i] - 'a'], s[i] = s[i - 1] + (f[i] = A.cnt[u]), g[i] = A.mxlen[u].second;
SMT::build(1, 1, m);
for (int i = 1; i <= n; ++i) {
ans[i].resize(t[i].length() + 1);
for (int j = 0, u = 1; j < t[i].length(); ++j)
ans[i][j + 1] = ans[i][j] + B.cnt[u = B.ch[u][t[i][j] - 'a']];
}
while (q--) {
int l, r;
cin >> l >> r;
int p = SMT::query(1, 1, m, l, r);
cout << (~p ? s[r] - s[p] + ans[g[p]][p - l + 1] : s[r] - s[l - 1]) << ' ';
}
return 0;
}
转移图上的 DP
P2292 [HNOI2004] L 语言
给定 \(n\) 个模式串 \(S_{1 \sim n}\) 和 \(m\) 个文本串 \(T_{1 \sim m}\) ,对于每个文本串,求出其最长的前缀,满足该前缀由若干模式串(可重复使用)拼接而成。
\(n \le 20\) ,\(m \le 50\) ,\(|S_i| \le 20\) ,\(|T_i| \le 2 \times 10^6\)
先对模式串建出 ACAM,然后将 \(T_i\) 放在 ACAM 上跑匹配。设当前匹配到的状态为 \(u\) ,若 \(u\) 在 fail 树上到根的路径上存在一个模式串的终止节点,则说明当前前缀具有一个后缀为模式串。
设 \(f_i\) 表示 \(i\) 的前缀是否能被模式串拼接出来,转移直接跳 fail 即可,单次询问复杂度 \(O(|T| \times \max |S_i|)\) ,并不优秀。
注意到 \(\max |S_i| \le 10\) ,考虑状压,对每个状态 \(u\) 记录 \(g_u\) 表示 \(u\) 跳 fail 到根路径上的终止节点状态,\(g_u\) 的第 \(i\) 位有值当且仅当链上存在一个长度为 \(i\) 的终止节点。
转移时只要记录前 \(\max |S_i|\) 位的 DP 值,若存在 \(f_j = \mathrm{true}\) 且 \((i - j) \in g_u\) 的状态,则 \(f_i = \mathrm{true}\) ,单次询问复杂度线性。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7, S = 26, All = (1 << 20) - 1;
char str[N];
int n, m;
namespace ACAM {
int ch[N][S], fail[N], edlen[N], g[N];
bool f[N];
int tot = 1;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
edlen[u] = n;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), g[u] = g[fail[u]] | (1 << edlen[u]);
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
}
inline int query(char *str, int n) {
f[0] = true;
int u = 1, state = 1, ans = 0;
for (int i = 1; i <= n; state = (state | f[i++]) & All) {
f[i] = (state <<= 1) & g[u = ch[u][str[i] - 'a']];
if (f[i])
ans = i;
}
return ans;
}
} // namespace ACAM
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", str + 1), ACAM::insert(str, strlen(str + 1));
ACAM::build();
while (m--) {
scanf("%s", str + 1);
printf("%d\n", ACAM::query(str, strlen(str + 1)));
}
return 0;
}
CF86C Genetic engineering
给出 \(m\) 个模式串 \(S_{1 \sim m}\) ,定义一个字符串 \(T\) 是好的,当且仅当对于 \(T\) 的每个位置 \(i\) ,都存在至少一个模式串 \(S_j\) 满足 \(T[l, r] = S_j\) ,其中 \(1 \le l \le i \le r \le |T|\) 。
求长度为 \(n\) 的好的字符串数量 \(\bmod (10^9 + 9)\) 。
\(m, |S_i| \le 10\) ,\(n \le 1000\) ,字符集为 \(\{A, C, G, T \}\)
不难发现合法条件等价于所有模式串的匹配段可以覆盖整个串。
对模式串建出 ACAM ,设 \(f_{u, i, j}\) 表示当前在状态 \(u\) ,构造了长度为 \(i\) 的串,仍有长度为 \(k\) 的后缀未匹配,预处理 \(match_u\) 表示 \(u\) 能匹配的最长长度,则:
- 若 \(j < match_{tr(u, c)}\) ,则 \(f_{u, i, j} \to f_{tr(u, c), i + 1, 0}\) 。
- 否则若 \(j + 1 \le \max |S|\) ,则 \(f_{u, i, j} \to f_{tr(u, c), i + 1, j + 1}\) 。
时间复杂度 \(O(nm|S| \times |\sum|)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 9;
const int N = 1e3 + 7, M = 11, S = 4;
char str[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int trans(char c) {
if (c == 'A')
return 0;
else if (c == 'C')
return 1;
else if (c == 'G')
return 2;
else
return 3;
}
namespace ACAM {
int ch[N][S], match[N], fail[N], f[N][N][M];
bool vis[N][N][M];
int tot = 1;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = trans(str[i]);
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
match[u] = n;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), match[u] = max(match[u], match[fail[u]]);
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
}
inline int solve(int n) {
queue<tuple<int, int, int> > q;
f[1][0][0] = 1, q.emplace(1, 0, 0), vis[1][0][0] = true;
int ans = 0;
while (!q.empty()) {
int u = get<0>(q.front()), i = get<1>(q.front()), j = get<2>(q.front());
q.pop();
if (i == n) {
if (!j)
ans = add(ans, f[u][i][j]);
continue;
}
for (int c = 0; c < S; ++c)
if (ch[u][c]) {
int v = ch[u][c];
if (j < match[v]) {
f[v][i + 1][0] = add(f[v][i + 1][0], f[u][i][j]);
if (!vis[v][i + 1][0])
q.emplace(v, i + 1, 0), vis[v][i + 1][0] = true;
} else if (j + 1 < M) {
f[v][i + 1][j + 1] = add(f[v][i + 1][j + 1], f[u][i][j]);
if (!vis[v][i + 1][j + 1])
q.emplace(v, i + 1, j + 1), vis[v][i + 1][j + 1] = true;
}
}
}
return ans;
}
} // namespace ACAM
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%s", str + 1), ACAM::insert(str, strlen(str + 1));
ACAM::build();
printf("%d", ACAM::solve(n));
return 0;
}
CF696D Legen...
给定 \(n\) 个字符串 \(S_{1 \sim n}\) ,每个字符串有权值 \(a_i\) 。定义一个字符串 \(T\) 的价值为 \(\sum_{i = 1}^n \mathrm{count(T, S_i)} \times a_i\) ,其中 \(\mathrm{count}(T, S_i)\) 表示 \(T\) 在 \(S_i\) 中的出现次数。
求所有长度为 \(l\) 的字符串的最大价值。
\(n, \sum|S_i| \le 200\) ,\(l \le 10^{14}\)
对模式串建立 ACAM,预处理出每个状态的权值 \(val\) 。设 \(f_{u, i}\) 表示当前在状态 \(u\) ,长度为 \(i\) 的串的最大权值,则:
直接做是 \(O(l \sum |S_i| \times |\sum|)\) 的,考虑优化。
注意到 \(l\) 很大,并且 \(\sum |S_i|\) 很小,而转移的形式类似于图上移动,因此考虑矩阵快速幂优化,即可做到 \(O((\sum |S_i|)^3 \log l)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e2 + 7, S = 26;
int a[N];
char str[N];
ll m;
int n;
namespace ACAM {
int ch[N][S], fail[N], val[N];
int tot = 1;
inline void insert(char *str, int n, int k) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
val[u] += k;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), val[u] += val[fail[u]];
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
}
} // namespace ACAM
struct Matrix {
ll a[N][N];
inline Matrix() {
memset(a, -inf, sizeof(a));
}
inline Matrix operator * (const Matrix &rhs) {
Matrix res;
for (int i = 1; i <= ACAM::tot; ++i)
for (int j = 1; j <= ACAM::tot; ++j)
for (int k = 1; k <= ACAM::tot; ++k)
res.a[i][k] = max(res.a[i][k], a[i][j] + rhs.a[j][k]);
return res;
}
inline Matrix operator ^ (ll b) {
Matrix res, base = *this;
for (int i = 1; i <= ACAM::tot; ++i)
res.a[i][i] = 0;
for (; b; base = base * base, b >>= 1)
if (b & 1)
res = res * base;
return res;
}
} f;
signed main() {
scanf("%d%lld", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
scanf("%s", str + 1), ACAM::insert(str, strlen(str + 1), a[i]);
ACAM::build();
for (int i = 1; i <= ACAM::tot; ++i)
for (int j = 0; j < S; ++j)
f.a[i][ACAM::ch[i][j]] = ACAM::val[ACAM::ch[i][j]];
f = f ^ m;
printf("%lld", *max_element(f.a[1] + 1, f.a[1] + ACAM::tot + 1));
return 0;
}
P2444 [POI2000] 病毒
给定 \(n\) 个 01 串 \(S_{1 \sim n}\) ,求是否存在无限长的串使得任何 \(S_i\) 均不为其子串。
\(n \le 3 \times 10^4\)
考虑构建出 \(S_{1 \sim n}\) 的 AC 自动机,并将每个 \(S_i\) 对应结束节点在 Fail 树上的整个子树打上标记表示不能走到。问题转化为是否存在一条无限长的路径满足其不经过任何打标记的点。
考虑将未打上标记的点和它们之间的边拿出来,如果从起点出发能够到达的是一个 DAG,那么不存在这样的路径,否则只要走到一个环,就可以构造出一个无限长的路径。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e4 + 7, S = 2;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int indeg[N];
char str[N];
int n;
namespace ACAM {
int ch[N][S], fail[N];
bool ed[N];
int tot = 1;
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] & 15;
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
ed[u] = true;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), ed[u] |= ed[fail[u]];
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
for (int i = 1; i <= tot; ++i)
if (!ed[i]) {
for (int j = 0; j < S; ++j)
if (!ed[ch[i][j]])
G.insert(i, ch[i][j]), ++indeg[ch[i][j]];
}
}
inline bool TopoSort() {
queue<int> q;
for (int i = 1; i <= tot; ++i)
if (!ed[i] && !indeg[i])
q.emplace(i);
int cnt = 0;
while (!q.empty()) {
int u = q.front();
q.pop(), ++cnt;
for (int v : G.e[u]) {
--indeg[v];
if (!indeg[v])
q.emplace(v);
}
}
return cnt == count(ed + 1, ed + tot + 1, false);
}
} // namespace ACAM
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str + 1);
ACAM::insert(str, strlen(str + 1));
}
ACAM::build();
puts(ACAM::TopoSort() ? "NIE" : "TAK");
return 0;
}
SP1676 GEN - Text Generator
给出 \(n\) 个字符串,求有多少长度为 \(L\) 的字符串满足存在这 \(n\) 个字符串中任意一个串作为子串。
\(n \le 10\) ,\(|S_i| \le 6\) ,\(L \le 10^6\)
先补集转化为不存在任意一个子串的方案数,对这 \(n\) 个串建立 ACAM,则构造字符串转化为转移图上的移动,”不存在任意一个子串“可以转化为若干点不可达。
设 \(f_{u, i}\) 表示当前在 \(u\) ,已经构造了长度为 \(i\) 的串,不难用矩阵快速幂优化转移做到 \(O((\sum |S_i|)^3 \log L)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e4 + 7;
const int N = 6e1 + 7, S = 26;
char str[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
namespace ACAM {
int ch[N][S], fail[N], idx[N];
bool ed[N];
int tot, cnt;
inline void clear() {
memset(ch + 1, 0, sizeof(int) * S * tot);
memset(fail + 1, 0, sizeof(int) * tot);
memset(ed + 1, false, sizeof(bool) * tot);
tot = 1;
}
inline void insert(char *str, int n) {
int u = 1;
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'A';
if (!ch[u][c])
ch[u][c] = ++tot;
u = ch[u][c];
}
ed[u] = true;
}
inline void build() {
fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop(), ed[u] |= ed[fail[u]];
for (int i = 0; i < S; ++i) {
if (ch[u][i])
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
else
ch[u][i] = ch[fail[u]][i];
}
}
cnt = 0;
for (int i = 1; i <= tot; ++i)
if (!ed[i])
idx[i] = ++cnt;
}
} // namespace ACAM
struct Matrix {
int a[N][N];
inline Matrix() {
memset(a, 0, sizeof(a));
}
inline Matrix operator * (const Matrix &rhs) {
Matrix res;
for (int i = 1; i <= ACAM::cnt; ++i)
for (int j = 1; j <= ACAM::cnt; ++j)
for (int k = 1; k <= ACAM::cnt; ++k)
res.a[i][k] = add(res.a[i][k], 1ll * a[i][j] * rhs.a[j][k] % Mod);
return res;
}
inline Matrix operator ^ (int b) {
Matrix res, base = *this;
for (int i = 1; i <= ACAM::cnt; ++i)
res.a[i][i] = 1;
for (; b; base = base * base, b >>= 1)
if (b & 1)
res = res * base;
return res;
}
};
signed main() {
while (~scanf("%d%d", &n, &m)) {
ACAM::clear();
for (int i = 1; i <= n; ++i)
scanf("%s", str + 1), ACAM::insert(str, strlen(str + 1));
ACAM::build();
Matrix f;
for (int i = 1; i <= ACAM::tot; ++i)
if (!ACAM::ed[i]) {
for (int j = 0; j < S; ++j)
if (!ACAM::ed[ACAM::ch[i][j]])
++f.a[ACAM::idx[i]][ACAM::idx[ACAM::ch[i][j]]];
}
f = f ^ m;
int ans = mi(S, m);
for (int i = 1; i <= ACAM::cnt; ++i)
ans = dec(ans, f.a[1][i]);
printf("%d\n", ans);
}
return 0;
}
CF1110H Modest Substrings
给定 \(l, r, n\) ,定义一个数的权值为数对 \((i, j) (i < j)\) 的数量,满足 \(i \sim j\) 位的数拿出来得到的数的值 \(\in [l, r]\) 。
求所有 \(n\) 位数的最大权值,并给出达到最大权值的最小数(可以有前导 \(0\) )。
\(l, r \le 10^{800}\) ,\(n \le 2000\)
显然有一个暴力,对 \(l \sim r\) 的所有数字串建立 AC 自动机,然后在上面做 DP。设 \(f_{u, i}\) 表示在 \(u\) 处组成长度为 \(i\) 的字符串的最大权值,预处理 \(g_{u, i}\) 表示当前状态自身的贡献即可做到快速转移。
不难发现上述做法瓶颈在于 AC 自动机状态数太多,而 Trie 树上有很多的子树都是满十叉树,这些子树内任意填都合法。
考虑将所有满十叉树缩成终止点,预处理满十叉树的贡献。
类似数位 DP,分讨建立 Trie 树:
- \(l, r\) 位数相等:
- 当前状态是 \(l, r\) 的共同前缀:下一状态为终止点当且仅当转移边 \(\in (l_i, r_i)\) 。
- 当前状态只是 \(l\) 的前缀:下一状态为终止点当且仅当转移边 \(\in (l_i, 9]\) 。
- 当前状态只是 \(r\) 的前缀:下一状态为终止点当且仅当转移边 \(\in [0, r_i)\) 。
- 当前状态是 \(l, r\) 的终点:直接对当前点记录贡献。
- \(l, r\) 位数不等:
- 当前状态只是 \(l\) 的前缀:下一状态为终止点当且仅当转移边 \(\in (l_i, 9]\) 。
- 当前状态只是 \(r\) 的前缀:下一状态为终止点当且仅当转移边 \(\in [0, r_i)\) 。
- 当前状态是 \(l, r\) 的终点:直接对当前点记录贡献。
- 此时对于长度 \(\in (len_l, len_r)\) 的串显然都合法,因此要对每一个这样的状态记录贡献。
时间复杂度 \(O(n |\sum|^2 \log r)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7, S = 10;
char L[N], R[N];
int n, lenl, lenr;
namespace ACAM {
const int M = 2e4 + 7;
int ch[M][S], fail[M], f[M][N], g[M][N];
bool vis[M][N];
int tot = 1;
inline int getnode(int x, int c) {
if (!ch[x][c])
ch[x][c] = ++tot;
return ch[x][c];
}
inline void update(int x, int l, int r, int len) {
for (int c = l; c <= r; ++c)
++g[getnode(x, c)][len];
}
inline void build() {
int rtl = 1, rtr = 1;
if (lenl == lenr) {
for (int i = 1; i <= lenl; ++i) {
int numl = L[i] & 15, numr = R[i] & 15;
if (rtl == rtr)
update(rtl, numl + 1, numr - 1, lenl - i);
else
update(rtl, numl + 1, 9, lenl - i), update(rtr, 0, numr - 1, lenr - i);
rtl = getnode(rtl, numl), rtr = getnode(rtr, numr);
}
++g[rtl][0];
if (rtl != rtr)
++g[rtr][0];
} else {
for (int i = 1; i <= lenl; ++i) {
int numl = L[i] & 15;
update(rtl, numl + 1, 9, lenl - i), rtl = getnode(rtl, numl);
}
for (int i = 1; i <= lenr; ++i) {
int numr = R[i] & 15;
update(rtr, 0, numr - 1, lenr - i), rtr = getnode(rtr, numr);
}
++g[rtl][0], ++g[rtr][0];
for (int i = lenl + 1; i < lenr; ++i)
for (int c = 1; c < S; ++c)
++g[getnode(1, c)][i - 1];
}
ch[1][0] = 0, fill(ch[0], ch[0] + S, 1);
queue<int> q;
q.emplace(1);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < S; ++i) {
if (ch[u][i]) {
fail[ch[u][i]] = ch[fail[u]][i], q.emplace(ch[u][i]);
for (int j = 0; j <= n; ++j)
g[ch[u][i]][j] += g[fail[ch[u][i]]][j];
} else
ch[u][i] = ch[fail[u]][i];
}
}
for (int u = 1; u <= tot; ++u)
for (int i = 0; i <= n; ++i)
g[u][i] += g[u][i - 1];
}
inline void solve() {
memset(f, -1, sizeof(f)), f[1][0] = 0;
for (int i = 0; i <= n; ++i)
for (int u = 1; u <= tot; ++u) {
if (f[u][i] == -1)
continue;
f[u][i] += g[u][n - i];
for (int c = 0; c < S; ++c)
f[ch[u][c]][i + 1] = max(f[ch[u][c]][i + 1], f[u][i]);
}
int ans = 0;
for (int u = 1; u <= tot; ++u)
if (f[u][n] > ans)
ans = f[u][n];
printf("%d\n", ans);
for (int u = 1; u <= tot; ++u)
if (f[u][n] == ans)
vis[u][n] = true;
for (int i = n - 1; ~i; --i)
for (int u = 1; u <= tot; ++u) {
if (f[u][i] == -1)
continue;
for (int c = 0; c < S; ++c)
if (vis[ch[u][c]][i + 1] && f[ch[u][c]][i + 1] == f[u][i] + g[ch[u][c]][n - i - 1]) {
vis[u][i] = true;
break;
}
}
for (int i = 0, u = 1; i < n; ++i)
for (int c = 0; c < S; ++c)
if (vis[ch[u][c]][i + 1] && f[ch[u][c]][i + 1] == f[u][i] + g[ch[u][c]][n - i - 1]) {
putchar(c | '0'), u = ch[u][c];
break;
}
}
} // namespace ACAM
signed main() {
scanf("%s%s%d", L + 1, R + 1, &n);
lenl = strlen(L + 1), lenr = strlen(R + 1);
ACAM::build(), ACAM::solve();
return 0;
}