AC 自动机
AC 自动机
自动机 (DFA)
「自动机」的英文是 Automaton .
OI 中「自动机」一般指「确定性有限状态自动机」(Deterministic Finite Automaton,DFA)
基本定义
DFA 一般是识别字符串,一个自动机 ,若他能识别字符串 ,则 ,否则 .
然后这个识别咋定义呢?当一个自动机读入一个字符串时,从初始状态(根节点)起按照转移函数一个一个字符地转移 . 如果读入完一个字符串的所有字符后位于一个接受状态,那么我们称这个自动机 接受 这个字符串,反之我们称这个自动机 不接受 这个字符串 .(这个接受其实就是识别吧)
形式化定义
其实弄懂了基本定义,形式化定义就很明了了 .
一个 DFA 由如下五个东西
- 状态集合 .
- 字符集 .
- 状态转移函数 .
- 一个开始状态 (即根节点)
- 接收状态集合 .
组成的五元组 .
把不能转移的都定义成 .
你定义一个 ,然后就可以定义接受 了,实际上这个新的 (「ex - 」)也就是沿着旧 走的过程 .
常见自动机
- Trie:转移函数就是 Trie 上一条边,其能接受的字符串就是插入到 Trie 中的字符串(或者其前缀,这取决于怎么定义接受)
- 子序列自动机:能接受的字符串是给定字符串的所有子序列,转移函数 是在字符 对应的 里 得到的返回值 . 每个节点都可以看作接受状态 .
- KMP 自动机:由 构造 的自动机能接受的字符串是以 为后缀的串 ,转移函数就是不断跳 next 的过程(形式化的,here)
然后就是我们现在要说的 AC 自动机,还有比较牛逼的(广义)后缀自动机(SAM),回文自动机(PAM)啥的,然而我不会啊 qwq
AC 自动机基本原理
AC 自动机的全称是 Aho-Corasick Automaton .
其中 Aho-Corasick 是人名 Alfred Aho 和 Margaret Corasick .
简单来说就是 Trie 树上 KMP .
这里我们的状态集合 是 Trie 上的所有节点 .
失配指针(fail)
定义: 的 fail 指针 指向其表示字符串的最长后缀在原 Trie 树上的节点 .
构建类似 KMP 的过程(因为是树上的玩意,所以要 BFS(这里不能 DFS 原因是其实这玩意是个「字典图」,后面说)) .
KMP 匹配的时候是不是要跳 next,然而然后我们可以真的把自动机建出来,这样就没 fail 啥事了,直接跳就完了 .
相当于把 Trie 树补成 Trie 图
// Sig 是字符集大小 .
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);}
else tr[u][i] = tr[fail[u]][i];
}
}
}
是不是非常简单~
后面说多模匹配 .
匹配的串串个数
跳跳跳跳跳,看看后缀 .
inline int query(string s)
{
int u = 0, ans = 0, l = s.length();
for (int i=0; i<l; i++)
{
u = tr[u][trans(s[i])];
for (int j=u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
} return ans;
}
整体代码可以看 洛谷模板简单版
串串出现的个数
建出 fail 树,记录自动机上的每个状态被匹配了几次,最后求出每个模式串在 Trie 上的终止节点在 fail 树上的子树总匹配次数就可以了 .
这是 ouuan 大佬说的,那些拓扑排序优化啥的看 SoyTony 的博客 .
代码看洛谷模板二次加强(其实自己写也很容易的啦 -)
然后这个玩意有个比较牛逼的写法(不用 DFS):here
时间复杂度
.
瓶颈在 build
.
例题
洛谷模板
加强版没写 .
简单版
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 26;
struct AC
{
int tr[N][Sig], fail[N], mark[N], cc, root;
inline void insert(string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
if (!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++cc;
u = tr[u][s[i] - 'a'];
} ++mark[u];
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);} // trie
else tr[u][i] = tr[fail[u]][i]; // automaton
}
}
}
inline int query(string s)
{
int u = root, ans = 0, l = s.length();
for (int i=0; i<l; i++)
{
u = tr[u][s[i] - 'a'];
for (int j = u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
} return ans;
}
inline void clear(){memset(tr, 0, sizeof tr); memset(mark, 0, sizeof mark); cc = 0;}
AC(){root = cc = 0;}
}ac;
int main()
{
int T = 1;
while (T--)
{
ac.clear(); int n; string tmp;
scanf("%d", &n);
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
cin >> tmp; ac.build();
printf("%d\n", ac.query(tmp));
} return 0;
}
二次加强版
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 26;
int n;
inline int trans(const char c){return c - 'a';}
struct SimpleGraph
{
vector<int> g[N];
inline void addedge(int u,int v){g[u].emplace_back(v);}
inline void ade(int u,int v){addedge(u, v); addedge(v, u);}
vector<int>& operator [](const int& idx){return g[idx];}
inline vector<int>& out_edges(int u){return g[u];}
};
struct AC
{
SimpleGraph FT; // fail tree
int tr[N][Sig], fail[N], mark[N], ed[N], siz[N], cc, root;
inline void insert(int id, string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} ++mark[u]; ed[id] = u;
}
inline void build()
{
queue<int> q;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i];}
else tr[u][i] = tr[fail[u]][i];
}
}
}
void dfs(int u)
{
for (int v : FT[u]) dfs(v), siz[u] += siz[v];
}
inline void query(string s)
{
int u = 0, l = s.length();
for (int i=0; i<l; i++){u = tr[u][trans(s[i])]; ++siz[u];}
for (int i=1; i<=cc; i++) FT.addedge(fail[i], i);
dfs(0);
}
AC(){cc = root = 0;}
}ac;
int main()
{
#ifndef ONLINE_JUDGE
freopen("i.in", "r", stdin);
#endif
int n; string tmp;
scanf("%d", &n);
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(i, tmp);}
ac.build();
cin >> tmp; ac.query(tmp);
for (int i=1; i<=n; i++) printf("%d\n", ac.siz[ac.ed[i]]);
return 0;
}
ed
数组的必要性:你至少得记下来这 个字符串都是啥呗 .
Keywords Search
就是 匹配的串串个数(和 洛谷模板简单版 是同一个题)
玄武密码
咋做都行 .
我是 insert
的时候每个点都标记上结尾标记,相当于把这玩意的所有前缀的都插进 Trie 里了 .
然后是不是就随便做了?
Code
using namespace std;
typedef long long ll;
const int N = 1e6 + 50, Sig = 4;
int n;
string s[N];
inline int trans(const char c)
{
if (c == 'E') return 0;
if (c == 'S') return 1;
if (c == 'W') return 2;
if (c == 'N') return 3;
return 114514;
}
struct AC
{
int tr[N][Sig], fail[N], mark[N], cc, root;
inline void insert(string s)
{
int u = root, l = s.length(); ++mark[root];
for (int i=0; i<l; i++)
{
if (!tr[u][trans(s[i])]) tr[u][trans(s[i])] = ++cc;
u = tr[u][trans(s[i])]; ++mark[u];
}
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]);}
else tr[u][i] = tr[fail[u]][i];
}
}
}
inline int query(string s)
{
int u = root, ans = 0, l = s.length();
for (int i=0; i<l; i++)
{
u = tr[u][trans(s[i])];
for (int j = u; j && ~mark[j]; j = fail[j]){ans += mark[j]; mark[j] = -1;}
} return ans;
}
inline int Q(string s)
{
int u = 0, l = s.length(), ans = -1;
for (int i=0; i<l; i++)
{
u = tr[u][trans(s[i])];
if (!~mark[u]) ans = i;
} return ans;
}
inline void clear(){memset(tr, 0, sizeof tr); memset(mark, 0, sizeof mark); cc = 0;}
AC(){root = cc = 0;}
}ac;
int main()
{
scanf("%d", &n); scanf("%d", &n);
cin >> s[0];
for (int i=1; i<=n; i++){cin >> s[i]; ac.insert(s[i]);}
ac.build(); ac.query(s[0]);
for (int i=1; i<=n; i++) printf("%d\n", ac.Q(s[i])+1);
return 0;
}
单词
所有字符串中间随便加个字符隔开,然后就跑二次加强版就完了 .
Code
感谢 APJ 大佬的指导 .
你加的那个字符是要算进字符集的,所以 Sig
不能开太小,字符也要选一个好的(e.g. z
+ 1) .
using namespace std;
typedef long long ll;
const int N = 1e6 + 222, Sig = 30;
int n;
inline int trans(const char c){return c - 'a';}
struct SimpleGraph
{
vector<int> g[N];
inline void addedge(int u,int v){g[u].emplace_back(v);}
inline void ade(int u,int v){addedge(u, v); addedge(v, u);}
vector<int>& operator [](const int& idx){return g[idx];}
inline vector<int>& out_edges(int u){return g[u];}
};
struct AC
{
SimpleGraph FT; // fail tree
int tr[N][Sig], fail[N], mark[N], ed[N], siz[N], cc, root;
inline void insert(int id, string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} ++mark[u]; ed[id] = u;
}
inline void build()
{
queue<int> q;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i];}
else tr[u][i] = tr[fail[u]][i];
}
}
}
void dfs(int u){for (int v : FT[u]) dfs(v), siz[u] += siz[v];}
inline void query(string s)
{
int u = 0, l = s.length();
for (int i=0; i<l; i++){u = tr[u][trans(s[i])]; ++siz[u];}
for (int i=1; i<=cc; i++) FT.addedge(fail[i], i);
dfs(0);
}
AC(){cc = root = 0;}
}ac;
int main()
{
#ifndef ONLINE_JUDGE
freopen("i.in", "r", stdin);
#endif
int n; string tmp, s;
scanf("%d", &n);
for (int i=1; i<=n; i++){cin >> tmp; s = s + "{" + tmp; ac.insert(i, tmp);}
ac.build(); ac.query(s);
for (int i=1; i<=n; i++) printf("%d\n", ac.siz[ac.ed[i]]);
return 0;
}
病毒
如果自动机上存在一个环,上面没有任何结束标记,且根到环没有结束标记,那么就可以构造出来 .
当然要根节点能到达,直接 DFS 即可 .
然后需要标记一下后缀,就是 mark[tr[u][i]] |= mark[fail[tr[u][i]]];
那句 .
有向图判环
好像可以拓扑排序,见 http://blog.sina.com.cn/s/blog_82a8cba50100yd46.html
这个 DFS 注意判过没环的就不用判了 .
其实可以用俩标记数组 vis
,cycle
实现等价功能 .
Code
using namespace std;
typedef long long ll;
const int N = 1e6 + 222, Sig = 2;
int n;
inline int trans(const char c){return c - '0';}
struct AC
{
int tr[N][Sig], fail[N], mark[N], root, cc;
inline void insert(string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} ++mark[u]; //
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i]; mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
else tr[u][i] = tr[fail[u]][i];
}
}
}
int cycle[N];
void dfs(int u)
{
cycle[u] = 1;
for (int i=0; i<Sig; i++)
{
int v = tr[u][i];
if (mark[v]) continue;
if (cycle[v] == 1){puts("TAK"); exit(0);} // find a cycle
if (!cycle[v]) dfs(v);
}
cycle[u] = -1;
}
AC(){cc = root = 0;}
}ac;
int main()
{
int n; string tmp;
scanf("%d", &n);
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
ac.build();
ac.dfs(ac.root);
puts("NIE");
return 0;
}
最短母串
不太算 dp .
考虑你在 AC 自动机上怎么找一个串串 有哪些模式串是它的子串 .
和上一个类似,我们可以整个 bitset(或者说是二进制状态压缩),然后对于每个节点按位或上 fail
更新即可
然后你 BFS 一下,然后找到全集就输出,是不是就完了?
BFS 的过程保证了字典序最小 .
然后要找这个串串还得记一个 pre
跳回去,/tuu/tuu/tuu
Code
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int N = 114514, Sig = 26;
int n;
inline int trans(const char c){return c - 'A';}
struct AC
{
int tr[N][Sig], fail[N], mark[N], root, cc;
inline void insert(int id, string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} mark[u] |= 1 << (id - 1);
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
if (tr[u][i]){q.push(tr[u][i]); fail[tr[u][i]] = tr[fail[u]][i]; mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
else tr[u][i] = tr[fail[u]][i];
}
}
unordered_set<int> vis;
basic_string<int> pre;
string ans;
inline void bfs()
{
pre.push_back('?'); ans.push_back('$');
queue<pii> q; // (node, state)
q.push(make_pair(root, 0)); vis.insert(0);
int now=0;
while (!q.empty())
{
pii _now = q.front(); q.pop();
int u = _now.first, s = _now.second;
if (s == ((1<<n)-1))
{
stack<char> st;
while (now){st.push(ans[now]); now = pre[now];}
while (!st.empty()){putchar(st.top()); st.pop();};
puts(""); exit(0);
}
for (int i=0; i<Sig; i++)
{
auto _ = tr[u][i] * N + (s | mark[tr[u][i]]);
if (vis.find(_) != vis.end()) continue; //
vis.insert(_);
pre.push_back(now); ans.push_back(i + 'A'); q.push(make_pair(tr[u][i], s | mark[tr[u][i]]));
}
++now;
}
}
AC(){cc = root = 0;}
}ac;
int main()
{
string tmp;
scanf("%d", &n);
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(i, tmp);}
ac.build(); ac.bfs();
return 0;
}
文本生成器
容斥(?)一下,变成 减掉不可读串串,看起来可做多了 .
首先必然建出所有了解单词的 AC 自动机 .
看看转移函数是啥意思?是不是有个自动跳 fail
?
于是标记一下不能完全匹配的,然后 AC 自动机上跳着转移即可 .
判断是否合法和 病毒 那题一模一样 .
Code
using namespace std;
typedef long long ll;
const int N = 114514, Sig = 26, P = 1e4+7;
inline int trans(const char c){return c - 'A';}
int n, m;
ll dp[123][12345];
struct AC
{
int tr[N][Sig], fail[N], root, cc;
bool mark[N];
inline void insert(string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} mark[u] = true;
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<Sig; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop();
for (int i=0; i<Sig; i++)
{
if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]); mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
else tr[u][i] = tr[fail[u]][i];
}
}
}
inline ll DP()
{
dp[0][0] = 1;
for (int i=0; i<m; i++)
for (int j=0; j<cc; j++)
for (int k=0; k<Sig; k++)
if (!mark[tr[j][k]]) dp[i+1][tr[j][k]] = (dp[i+1][tr[j][k]] + dp[i][j]) % P;
ll ans = 0;
for (int i=0; i<cc; i++) ans = (ans + dp[m][i]) % P;
return ans;
}
AC(){root = cc = 0;}
}ac;
int main()
{
string tmp;
scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
ac.build();
ll full = 1;
for (int i=0; i<m; i++) full = full * 26 % P; // full = 26^m
printf("%lld\n", (full - ac.DP() + P) % P);
return 0;
}
背单词
先建 AC 自动机,另一个串是一个串的子串等价于这个串的任意前缀能够通过 树到根的路径上走到另一个串 .
然后在 树上 DP .
不给代码!!!!!
密码
和 文本生成器 类似 .
建出所有模式串的 AC 自动机,然后注意到数据范围非常小,考虑状压 DP .
令 表示到第 位,自动机上是 ,包含集合 的模式串个数 .
于是转移就在 AC 自动机上走一步即可(刷表).
关于输出方案 — 注意到如果输出方案则答案不超过 ,于是暴搜即可 .
DP 转移咋写的暴搜就咋写,本质是相同的 .
Code
using namespace std;
typedef long long ll;
const int L = 27, N = 111, T = 1<<11, alphabet = 26;
inline int trans(const char c){return c - 'a';}
int n, m;
ll dp[L][N][T];
struct AC
{
int tr[N][alphabet], mark[N], fail[N], root, cc;
inline void insert(int id, string s)
{
int u = root, l = s.length();
for (int i=0; i<l; i++)
{
int _ = trans(s[i]);
if (!tr[u][_]) tr[u][_] = ++cc;
u = tr[u][_];
} mark[u] |= (1 << id);
}
inline void build()
{
queue<int> q; fail[root] = root;
for (int i=0; i<alphabet; i++)
{
if (tr[root][i]){q.push(tr[root][i]); fail[tr[root][i]] = root;}
else tr[root][i] = root;
}
while (!q.empty())
{
int u = q.front(); q.pop(); mark[u] |= mark[fail[u]];
for (int i=0; i<alphabet; i++)
{
if (tr[u][i]){fail[tr[u][i]] = tr[fail[u]][i]; q.push(tr[u][i]); mark[tr[u][i]] |= mark[fail[tr[u][i]]];}
else tr[u][i] = tr[fail[u]][i];
}
}
}
inline ll DP()
{
dp[0][0][0] = 1;
for (int i=0; i<n; i++)
for (int j=0; j<=cc; j++)
for (int s=0; s<(1<<m); s++)
{
if (!dp[i][j][s]) continue;
for (int k=0; k<alphabet; k++) // trans
{
int v = tr[j][k];
dp[i+1][v][s | mark[v]] += dp[i][j][s];
}
}
ll ans = 0;
for (int i=0; i<=cc; i++) ans += dp[n][i][(1<<m)-1];
return ans;
}
AC(){root = cc = 0;}
}ac;
bool vis[L][N][T], chk[L][N][T];
int mov[L];
bool dfs(int i, int j, int s)
{
if (i == n)
{
vis[i][j][s] = true;
return chk[i][j][s] = (s == (1<<m)-1);
}
if (vis[i][j][s]) return chk[i][j][s];
vis[i][j][s] = true;
bool ans = false;
for (int k=0; k<alphabet; k++)
ans |= dfs(i+1, ac.tr[j][k], s | ac.mark[ac.tr[j][k]]);
return chk[i][j][s] = ans;
}
void output(int i, int j, int s)
{
if (!chk[i][j][s]) return ;
if (i == n)
{
for (int p=1; p<=n; p++) putchar(mov[p] + 'a');
puts(""); return ;
}
for (int k=0; k<alphabet; k++)
{
mov[i+1] = k;
output(i+1, ac.tr[j][k], s | ac.mark[ac.tr[j][k]]);
}
}
int main()
{
string tmp; scanf("%d%d", &n, &m);
for (int i=1; i<=m; i++){cin >> tmp; ac.insert(i-1, tmp);}
ac.build();
ll ans = ac.DP();
printf("%lld\n", ans);
if (ans > 42) return 0;
dfs(0, 0, 0); output(0, 0, 0);
return 0;
}
禁忌
题目背景好评
这个 看着就不顺眼,改成 .
首先对模式串建出 AC 自动机 .
然后这个禁忌魔法的伤害只需要在 AC 自动机上贪心就可以了,比较平凡 .
考虑 DP,令 表示到第 个字符,在 AC 自动机上到第 个字符的概率,这是 AC 自动机上 DP 的常见形式 .
考虑在 AC 自动机上走一步 进行转移,于是
然而我们的划分不能重复,也就是样例解释里说的那个东西 .
于是如果 是某个模式串的末尾(也就是匹配上了),我们就令 (相当于回到起始点重新匹配).
当然我们这个 数组是概率,我们要在 DP 过程中统计期望 .
直接大力 DP 显然是过不去的,因为 贼大 .
然而这个东西相当于同样的转移跑 次,可以直接矩阵快速幂优化掉,这个和 GT 考试 的手法类似 .
然后时间复杂度瞬间变成 ,轻松跑过 .
details: 加一维用来统计期望 .
Core Code:
Matrix<db> D, ans;
ACAM ac;
int n, len, ab;
db one = 1.0;
int main()
{
scanf("%d%d%d", &n, &len, &ab); string tmp;
for (int i=1; i<=n; i++){cin >> tmp; ac.insert(tmp);}
ac.build();
int S = ac.size();
for (int i=0; i<=S; i++)
for (int j=0; j<ab; j++)
{
int k = ac.tr[i][j];
if (ac.mark[k]){D[S+1][i] += one/ab; D[0][i] += one/ab;}
else D[k][i] += one/ab;
}
D[S+1][S+1] = one; D ^= len;
ans[0][0] = 1; D *= ans; // !!!!!
printf("%.10f\n", D[S+1][0]);
return 0;
}
完整代码:R71833633 .
这个矩阵快速幂优化 DP 也可以看成 AC 自动机上走 步,这个先保留 .
在哪里看题 & 在哪里交
背单词是 BZOJ2905 .
Reference
- skyh 神仙的课件《字符串算法基础》
- 自动机 - OI Wiki
- AC 自动机 - OI Wiki
- Trie 图 - hihoCoder
- AC自动机学习笔记 - ouuan
- Trie图的构建、活用与改进 - Maigo
以下是博客签名,正文无关
本文来自博客园,作者:yspm,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/15968827.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
2021-03-05 浅谈拉格朗日插值