学习笔记:AC 自动机
耳机声音疑似有点小了,用心旷神怡的话来说大致会是「比果蝇↑嗡嗡声还小」。
?卧槽耳机上居然可以调音量
前置知识
首先可能需要知道专有名词「自动机」的含义。
大致可以简单理解为,一个 DAG,其中点表示状态,边表示转移。给进去一个字符串之类,就可以在 DAG 上游走,根据最后所处结点,可以得到字符串相关特征。
这个定义让我们想到了 Trie。事实上,Trie 就是一种相当基础的自动机。
接下来,让我们复习 KMP 相关概念。
假想一个场景:你需要求得 在 中的出现次数。
拥有一定题目经验的你,会使用 KMP 求出 的 next
数组,令 在 上进行匹配,失配或完全匹配时回到 在该处的 next
, 上的指针始终向右, 上的指针如果向右,每次只能移动一位;如果向左,最左移到开头,均摊下来复杂度即为 。
假如场景变得更复杂:给定 ,你需要求出每个 在 中的出现次数。
那么如果我们对每个 进行一次 KMP,复杂度将会上升到 ,难以承受。有没有优化的方法呢?
AC 自动机的建立
结合上面的知识,我们下意识想到,能不能将 建成一个树状结构,令 在其上进行 KMP 呢?
将 全部加入 Trie,由于 next
只会由更深的点指向更浅的点,似乎从直觉上是有规则的。但随之而来的是一个问题:点 的 next
不一定在 的链上(由于 Trie 的性质,可知指向的点是唯一的)。鉴于这一点不同,我们结合其「失配指针」的定义,将 next
在 Trie 上的同分异构体唤为 fail
。
考察 Trie 上该 fail
边的性质,假设其为 ,结合 KMP 中 next
的性质,假设 的父节点为 ,其 fail
为 ,那么有:
- 上有边权为 的边, 为该边右端结点。
- 否则,前往 的
fail
,重复以上判定。
那么我们就完成了失配的处理。那么相应地,完全匹配时的跳转应如何处理呢?我们直接暴力地将 fail
的儿子全部接到 下面就行了。如果 和 fail
具有同一条边呢?我们选择保留 的这条边 ,因为 的 fail
必定指向 的 fail
的对应儿子。
两点结合,我们发现从实现上,可以直接令 的 fail
指向 的 fail
的对应儿子(那么时间复杂度显而易见是 的)。
匹配时,对于每个点和其返回到根的 fail
链,全部标记。则一个模式串匹配的次数即为被打标记的次数。
那么根据上述要求,我们需要按照深度顺序求得 fail
,考虑 BFS。
哦哦好神奇复活之后记得啥是 KMP 但忘了啥是 C++ 了。甚至花了一点时间学习怎么创建一个函数???
大家写 AC 自动机 Trie 根节点下标一定要设成 0 啊 设成 1 被各种细节坑惨了
#include <bits/stdc++.h>
const int maxn = 2e5 + 5;
int T[maxn][26], tot, cnt[maxn], fail[maxn];
int ins(std::string &t) {
int p = 0;
for (auto i : t) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot;
p = T[p][i - 'a'];
}
return p;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n;
std::cin >> n;
std::vector<int> tail(n + 1);
std::vector<std::string> t(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> t[i], tail[i] = ins(t[i]);
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
std::string s;
std::cin >> s;
{
int p = 0;
for (auto i : s) {
p = T[p][i - 'a'];
for (int fa = p; fa; fa = fail[fa])
++cnt[fa];
}
}
for (int i = 1; i <= n; ++i)
std::cout << cnt[tail[i]] << '\n';
return 0;
}
肉眼可见该查询方式是极其低效的,故考虑优化跳 fail
打标记的过程。
既然自动机结构不变,不如将跳 fail
的步骤放在最后统一进行。容易在发现 fail
树上进行拓扑排序转移即可。
#include <bits/stdc++.h>
const int maxn = 2e5 + 5;
int T[maxn][26], tot, cnt[maxn], fail[maxn], deg[maxn];
int ins(std::string &t) {
int p = 0;
for (auto i : t) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot;
p = T[p][i - 'a'];
}
return p;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n;
std::cin >> n;
std::vector<int> tail(n + 1);
std::vector<std::string> t(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> t[i], tail[i] = ins(t[i]);
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i], ++deg[T[fail[u]][i]];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
std::string s;
std::cin >> s;
{
int p = 0;
for (auto i : s)
p = T[p][i - 'a'], ++cnt[p];
}
{
std::queue<int> q;
for (int i = 1; i <= tot; ++i)
if (!deg[i])
q.push(i);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
cnt[fail[u]] += cnt[u];
if (!--deg[fail[u]])
q.push(fail[u]);
}
}
for (int i = 1; i <= n; ++i)
std::cout << cnt[tail[i]] << '\n';
return 0;
}
AC 自动机的应用与识别
我们知道其最典型的特征是 多模式串、静态 / 离线。当碰到类似特点时,大概率就是 AC 自动机。
其中,可以设置的难点有:
- 字符串难点,和其他字符串题可设置的难点相同。
- fail 树维护,可能结合数据结构、拓扑排序、树形 DP 等考察。
- DP 的设计。
字符串难点设计
eg. Indie Album
https://codeforces.com/problemset/problem/1207/G
题目的「可持久化」试图误导我们用操作串建立自动机,但这样就会有一个比较严重的问题,我们没办法求 的出现次数。
为了保证答案可求我们仍然在 上建立 ACAM。容易发现操作串以 Trie 形式给出,我们可以在遍历 Trie 时同时完成游走,通过回溯完成询问。
假设当前 DFS 中,遍历到 Trie 树中的 点和自动机中的 状态,那么对于 点所对应的一个询问串 ,相当于询问经过的所有状态有多少个在 引导的 fail 树子树中。我们求出 fail 树的 dfn,用树状数组简单统计即可。
#include <bits/stdc++.h>
const int maxn = 4e5 + 5;
std::vector<int> g[maxn], q[maxn];
int fail[maxn], T[maxn][26], tot, bit[maxn], to[maxn][26], cnt[maxn];
int ins(std::string s) {
int p = 0;
for (auto i : s) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot;
p = T[p][i - 'a'];
}
return p;
}
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (; x <= tot + 1; x += lowbit(x))
bit[x] += v;
return;
}
int ask(int x) {
int res = 0;
for (; x; x -= lowbit(x))
res += bit[x];
return res;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, now = 0;
std::cin >> n;
std::vector<int> id(n + 1);
for (int i = 1, op; i <= n; ++i) {
char t;
std::cin >> op;
if (op == 1) {
std::cin >> t;
if (!to[0][t - 'a'])
to[0][t - 'a'] = ++now;
++cnt[to[0][t - 'a']], id[i] = to[0][t - 'a'];
}
else {
int j;
std::cin >> j >> t;
if (!to[id[j]][t - 'a'])
to[id[j]][t - 'a'] = ++now;
++cnt[to[id[j]][t - 'a']], id[i] = to[id[j]][t - 'a'];
}
}
int m;
std::cin >> m;
std::vector<int> tail(m + 1), res(m + 1);
for (int i = 1, x; i <= m; ++i) {
std::string t;
std::cin >> x >> t, tail[i] = ins(t);
q[id[x]].push_back(i);
}
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]), g[0].push_back(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i];
q.push(v), g[fail[v]].push_back(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
std::vector<int> dfn(tot + 1), rfn(tot + 1);
std::function<void(int)> DFS = [&](int x) {
static int now = 0;
dfn[x] = ++now;
// printf("%d ", x);
for (auto i : g[x])
DFS(i);
rfn[x] = now;
return;
};
DFS(0);
std::function<void(int, int)> DFS1 = [&](int x, int u) {
add(dfn[u], 1);
for (auto i : q[x])
res[i] += ask(rfn[tail[i]]) - ask(dfn[tail[i]] - 1);
for (int i = 0; i < 26; ++i)
if (to[x][i])
DFS1(to[x][i], T[u][i]);
add(dfn[u], -1);
return;
};
DFS1(0, 0);
for (int i = 1; i <= m; ++i)
std::cout << res[i] << '\n';
return 0;
}
fail 树的维护
eg1. Divljak
https://www.luogu.com.cn/problem/P5840
法一:把动态问题离线
问题相当于将模式串 动态化,但我们仍可以无脑离线下来解决问题。
考虑原本的答案计算过程,即在经过的所有状态及其 fail 链上打标记。现在我们需要离线并区分标记的来源(并且标记类型为布尔值),下意识想到使用线段树维护。
我们对每个状态建立动态开点线段树,最后拓扑排序时使用线段树合并处理信息。容易证明时间复杂度相较原来多了一个 。
理论可行,开始实践 出题人似乎不是很喜欢线段树选手所以决定剥夺你的 Memory Limit。想要用这种做法通过本题可见 https://www.luogu.com.cn/article/jaxk3sno。
法二:转而处理静态问题
注意到题目中的静态的 比起 更适合用来做模式串,我们在 上构建 AC 自动机,考虑在线解决问题。
在每次 1
操作时,更新模式串信息。对于途径的所有状态 ,考虑更新其所在 fail 链上的信息。
注意到我们需要修改整条 fail 链上的信息,询问则是询问单点。这个时候可以考虑使用树上差分。
但是 here comes a problem,我们对于这一整个串只能在整个树上每个点上更新一次。怎么消去相同的影响呢?
这里实现上我们将经过的点按 fail 树上 dfn 排序,并且对于序列中相邻的两点在其 LCA 上减去一次标记。为什么这是正确的呢?参考虚树,我们只需要让序列中相邻的两个点尽量近就可以保证重复的被删除完毕。
#include <bits/stdc++.h>
const int maxn = 2e6 + 5;
int tot, bit[maxn], T[maxn][26], fail[maxn];
int ins(std::string &t) {
int p = 0;
for (auto i : t) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot;
p = T[p][i - 'a'];
}
return p;
}
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
// printf("add (%d, %d)\n", x, v);
for (; x <= tot + 1; x += lowbit(x))
bit[x] += v;
return;
}
int ask(int x) {
int res = 0, to = x;
for (; x; x -= lowbit(x))
res += bit[x];
// printf("ask(%d) = %d\n", to, res);
return res;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, q;
std::cin >> n;
std::vector<int> tail(n + 1);
for (int i = 1; i <= n; ++i) {
std::string t;
std::cin >> t, tail[i] = ins(t);
}
std::vector<std::vector<int> > g(tot + 2);
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
for (int i = 1; i <= tot; ++i)
g[fail[i] + 1].push_back(i + 1);
}
std::vector<std::array<int, 22> > f(tot + 2);
std::vector<int> dep(tot + 2), dfn(tot + 2), siz(tot + 2);
std::function<void(int)> DFS = [&](int x) {
static int now = 0;
siz[x] = 1, dfn[x] = ++now;
// printf("%d\n", x);
for (auto i : g[x]) {
dep[i] = dep[x] + 1;
f[i][0] = x;
for (int j = 1; j <= 21; ++j)
f[i][j] = f[f[i][j - 1]][j - 1];
DFS(i), siz[x] += siz[i];
}
return;
};
dep[1] = 1, DFS(1);
auto askLCA = [&](int x, int y) {
if (x == y)
return x;
if (dep[x] < dep[y])
std::swap(x, y);
for (int i = 21; ~i; --i)
if (dep[f[x][i]] >= dep[y])
x = f[x][i];
if (x == y)
return x;
for (int i = 21; ~i; --i)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
};
std::cin >> q;
for (; q--; ) {
int op;
std::cin >> op;
if (op == 1) {
std::string s;
std::cin >> s;
int p = 0, len = (int)s.length();
std::vector<int> id(len + 1);
for (int i = 1; i <= len; ++i)
p = T[p][s[i - 1] - 'a'], id[i] = p + 1;
std::sort(id.begin() + 1, id.end(), [&](int x, int y) { return dfn[x] < dfn[y]; });
// for (int i = 1; i <= len; ++i)
// printf("%d ", id[i]);
// puts("");
for (int i = 1; i <= len; ++i) {
// printf("%d %d %d\n", i, id[i], dfn[id[i]]);
// assert(0);
add(dfn[id[i]], 1);
if (i != 1)
add(dfn[askLCA(id[i], id[i - 1])], -1);
}
}
else {
int x;
std::cin >> x;
x = tail[x] + 1;
std::cout << ask(dfn[x] + siz[x] - 1) - ask(dfn[x] - 1) << '\n';
}
}
return 0;
}
eg2.
AC 自动机上的 DP
鉴于 AC 自动机的优秀结构与性质,并不经常作为字符串匹配工具出现,其一个应用是作为 DP 的载体。
eg1. L 语言
https://www.luogu.com.cn/problem/P2292
我们想到要在自动机上匹配,但此时 fail
作为「断句」的唯一手段(断句的位置在链上当前点深度 - fail
深度处),不再仅当失配时才能经过。我们考虑朴素的 DP:在经过的每个状态考虑断句,那么这要求断句处是一个单词的结尾,那么此时 fail
最长匹配长度即可被更新。最后遍历所有单词的末结点,取最大答案。这里的 fail
其实是 fail
链上任意一点。
考虑复杂度。容易发现对于每一个点我们跳了其整条 fail 链,那么复杂度就是最劣 的。考虑优化这个过程至 。
题目里有一个很重要的条件还没有用到:单个单词长度 ,这让我们想到状态压缩。对于每一个状态,记录其断出来单词的可能长度。
我们在 DAG 上游走的时候记录目前可以断的所有位置,如果其和当前可断出来的长度之交不为空,就可以将此处加入「可以断的所有位置」并更新答案。
#include <bits/stdc++.h>
const int maxn = 2e5 + 5;
int T[maxn][26], tot, fail[maxn], len[maxn], dep[maxn], tag[maxn];
int ins(std::string &t) {
int p = 0;
for (auto i : t) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot, dep[tot] = dep[p] + 1;
p = T[p][i - 'a'];
}
++tag[p];
return p;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, m;
std::cin >> n >> m;
std::vector<int> tail(n + 1);
std::vector<std::string> t(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> t[i], tail[i] = ins(t[i]);
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
len[u] = len[fail[u]];
if (tag[u])
len[u] |= (1 << dep[u]);
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
for (; m--; ) {
std::string s;
std::cin >> s;
int p = 0, q = 1, res = 0;
for (int i = 0; i < (int)s.length(); ++i) {
p = T[p][s[i] - 'a'], q <<= 1;
if (len[p] & q)
q |= 1, res = i + 1;
}
std::cout << res << '\n';
}
return 0;
}
eg2. Popcount Words
https://codeforces.com/gym/103409/problem/H
咋上强度了啊。
先把整个序列写出来,即 ,尝试进一步探究形式化的规律,可以得到:
- ,其中 表示 内,原串 / 取反的值。
- 对于不以 开头的整段,可以从上述规则转化为 开头的整段。
接着不难想到一种类似线段树的方式,将待求的 分到 个整段上,那么 就可以被 个整段(也是 个本质不同整段)描述。
我们对 建立 AC 自动机,需要知道这 个整段在每个点上的出现次数。对于自动机上任意状态 ,设 表示 经过 后到达的点,则可倍增(嘶,这里是不是应该反过来叫分治啊)简单解决。
那么接下来我们就可以用 来进行快速游走了。顺便打个 记录一下每个 作为不同整段的开头被经过的次数。然后做一个 DP,类似于线段树上 pushdown
的操作把所有整段下放到单点上的单个字符。
有一说一用 DP 来处理这个东西还挺难想的。可能也是基于前面的倍增吧。最后拓扑排序就行了。
大家数组一定要用 C-style array 啊,std::vector<>
计算的是申请空间包 MLE 的
大家大数组一定要内存连续访问优化啊,TLE 100ms 泪目了
#include <bits/stdc++.h>
const int maxn = 5e5 + 5;
long long sum[maxn], f[2][30][maxn];
int cnt[2][30][maxn], to[2][30][maxn];
int T[maxn][2], tot, fail[maxn], deg[maxn];
int ins(std::string &t) {
int p = 0;
for (auto i : t) {
if (!T[p][i - '0'])
T[p][i - '0'] = ++tot;
p = T[p][i - '0'];
}
return p;
}
void ask(std::vector<std::pair<int, int> > &s, int ql, int qr, int l = 0, int r = (1 << 30) - 1, int len = 30, int v = 0) {
if (ql <= l && r <= qr) {
s.emplace_back(len, v);
return;
}
int mid = l + (r - l) / 2;
if (ql <= mid)
ask(s, ql, qr, l, mid, len - 1, v);
if (qr > mid)
ask(s, ql, qr, mid + 1, r, len - 1, v ^ 1);
return;
}
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n, m;
std::cin >> n >> m;
std::vector<std::pair<int, int> > s;
for (int i = 1; i <= n; ++i) {
int l, r;
std::cin >> l >> r;
ask(s, l, r);
}
std::vector<int> tail(m + 1);
for (int i = 1; i <= m; ++i) {
std::string t;
std::cin >> t;
tail[i] = ins(t);
}
{
std::queue<int> q;
for (int i = 0; i < 2; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 2; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i], ++deg[T[fail[u]][i]];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
for (int i = 0; i <= tot; ++i)
to[0][0][i] = T[i][0], to[1][0][i] = T[i][1];
for (int j = 1; j < 30; ++j)
for (int i = 0; i <= tot; ++i) {
to[0][j][i] = to[1][j - 1][to[0][j - 1][i]];
to[1][j][i] = to[0][j - 1][to[1][j - 1][i]];
}
{
int p = 0;
for (auto [n, i] : s) {
// printf("# %d %d\n", n, i);
++cnt[i][n][p], p = to[i][n][p];
}
}
for (int j = 29; ~j; --j)
for (int i = 0; i <= tot; ++i) {
if (j != 29) {
f[0][j][i] += f[0][j + 1][i];
f[1][j][i] += f[1][j + 1][i];
f[0][j][to[1][j][i]] += f[1][j + 1][i];
f[1][j][to[0][j][i]] += f[0][j + 1][i];
}
f[1][j][i] += cnt[1][j][i];
f[0][j][i] += cnt[0][j][i];
}
for (int i = 0; i <= tot; ++i) {
sum[T[i][0]] += f[0][0][i], sum[T[i][1]] += f[1][0][i];
// printf("%d %d\n", f[i][0][0], f[i][0][1]);
}
{
std::queue<int> q;
for (int i = 0; i <= tot; ++i)
if (!deg[i])
q.push(i);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
sum[fail[u]] += sum[u];
if (!--deg[fail[u]])
q.push(fail[u]);
}
}
for (int i = 1; i <= m; ++i)
std::cout << sum[tail[i]] << '\n';
return 0;
}
eg3. Legen...
https://codeforces.com/problemset/problem/696/D
先在 fail 树上把每个状态的实际价值计算出来。我们发现匹配串是未知的,也就是我们需要主动决定游走路径。注意到 ,考虑矩阵。
令 表示在 状态时已经走了 步,可以得到的最大价值。那么显然有:
其中 是自动机上 的任意出边。图的大小为 ,可以放到 矩阵里加速转移。
#include <bits/stdc++.h>
const int maxn = 2e5 + 5;
const long long inf = 1e18;
int fail[maxn], T[maxn][26], tot;
int ins(std::string s) {
int p = 0;
for (auto i : s) {
if (!T[p][i - 'a'])
T[p][i - 'a'] = ++tot;
p = T[p][i - 'a'];
}
return p;
}
struct matrix {
int n, m;
std::vector<std::vector<long long> > a;
matrix(int n1, int m1, long long v = -inf, bool op = 0): n(n1), m(m1), a(n + 1, std::vector<long long> (m + 1, v)) {
if (op)
for (int i = 0; i <= n; ++i)
a[i][i] = 0;
return;
}
std::vector<long long> &operator[] (int i) {
return a[i];
}
matrix operator* (matrix &q) const {
matrix res(n, q.m);
for (int k = 0; k <= m; ++k)
for (int i = 0; i <= n; ++i)
for (int j = 0; j <= q.m; ++j)
res[i][j] = std::max(res[i][j], a[i][k] + q[k][j]);
return res;
}
matrix& operator*= (matrix q) {
return *this = *this * q;
}
matrix operator^ (long long q) {
matrix res(n, n, -inf, 1), x(*this);
for (; q; q >>= 1, x *= x)
if (q & 1)
res *= x;
return res;
}
};
int main() {
#ifdef ONLINE_JUDGE
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
std::freopen(".in", "r", stdin);
std::freopen(".out", "w", stdout);
#endif
int n;
long long m;
std::cin >> n >> m;
std::vector<long long> a(n + 1);
for (int i = 1; i <= n; ++i)
std::cin >> a[i];
std::vector<int> tail(n + 1);
std::vector<std::string> t(n + 1);
for (int i = 1; i <= n; ++i) {
std::cin >> t[i];
tail[i] = ins(t[i]);
}
std::vector<long long> s(tot + 1);
for (int i = 1; i <= n; ++i)
s[tail[i]] += a[i];
{
std::queue<int> q;
for (int i = 0; i < 26; ++i)
if (T[0][i])
q.push(T[0][i]);
for (; !q.empty(); ) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i)
if (T[u][i]) {
int v = T[u][i];
fail[v] = T[fail[u]][i], s[v] += s[fail[v]];
q.push(v);
}
else
T[u][i] = T[fail[u]][i];
}
}
matrix f(0, tot), op(tot, tot);
f[0][0] = 0;
for (int i = 0; i <= tot; ++i)
for (int j = 0; j < 26; ++j)
op[i][T[i][j]] = s[T[i][j]];
f *= (op * m);
std::cout << *std::max_element(f[0].begin(), f[0].end()) << '\n';
return 0;
}
eg4. You Are Given Some Strings...
goto link.
—— · EOF · ——
真的什么也不剩啦 😖
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】