AC自动机
Posted 1625--h
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AC自动机相关的知识,希望对你有一定的参考价值。
AC 自动机
1. Dominating Patterns
给N个串,然后再给一个串s,求N个串总共在S中出现了多少次
将N个串插入到AC自动机当中,如果某个结点为模式串末尾结点,则在fail树中,以该节点为祖先的所有结点,贡献都+1
然后直接暴力匹配即可,最后倒着去算一遍贡献。(具体体现在fail树中,如果一个结点匹配上了,那么他的祖先一定都可以匹配上,按序号降序去累加可以保证顺序是正确的,因为一个结点的fail指针所指结点的序号一定更小)
需要注意的是,N个串中可能有相同的。
const int N = 1000010 + 5;
int n;
char s[N], P[200][100];
map<string,int> mp;
namespace AC{
int tr[N][26],tot;
int e[N],fail[N];
int cnt[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = cnt[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
mp[string(s+1)] = u;
e[u] ++;
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
for(int i=0;i<26;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];
}
}
}
void get(char *t){
int u = 0,res = 0;
for(int i=1;t[i];i++){
u = tr[u][t[i] - 'a'];
cnt[u] ++;
}
for(int i=tot;i>=0;i--) cnt[fail[i]] += cnt[i];
for(int i=0;i<=tot;i++) if(e[i]){
res = max(res, cnt[i]);
}
printf("%d
", res);
for(int i=1;i<=n;i++){
if(cnt[mp[string(P[i]+1)]] == res) printf("%s
", P[i]+1);
}
}
}
int main() {
while(~scanf("%d",&n)){
if(n == 0) break;
mp.clear();
AC::init();
for(int i=1;i<=n;i++){
scanf("%s", P[i]+1);
AC::insert(P[i]);
}
AC::build();
scanf("%s",s+1);
AC::get(s);
}
return 0;
}
2. Substring
给 K 个串,再给一个大小为N的字符集,给出从N个字符中选第 i 个的概率 p_i, 然后每次从中选出一个字符,选L次拼成一个长为L的字符串,求K个串不出现在该串中的概率。
AC自动机中的trans函数,即状态转移函数,如果 (trans[u][c]) 所指向的结点是一个字符串的末尾结点或者是末尾结点在fail树上的子孙,那么该转移不能选择,如果选择则意味着有一个字符串会出现在构造的这个串中。
到这里就变成了一个概率DP的问题,为了降低编程难度,可以直接用记忆化搜索实现。
还需要注意的是字符集的问题,它包括了大小写以及数字。
const int N = 1010;
int n, k, L, vis[1010][200], in[300];
double p[300], d[1010][200];
char s[22][22];
namespace AC{
int tr[N][300],tot;
int e[N],fail[N];
int cnt[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = cnt[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i]]){
tr[u][s[i]] = ++tot;
}
u = tr[u][s[i]];
}
e[u] ++;
}
void build(){
for(int i=0;i<130;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] |= e[fail[u]]; // e[u]=1则表示u结点所表示的状态的某一个后缀对应K个字符串中的某个
for(int i=0;i<130;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];
}
}
}
double get(int u, int L){
if(!L) return 1.0;
if(vis[u][L]) return d[u][L];
vis[u][L] = 1;
double& ans = d[u][L];
ans = 0;
for(int i=0;i<130;i++){
//in[i] 则表示该字符出现在题目给定的字符集中
if(in[i] && !e[tr[u][i]]) ans += p[i] * get(tr[u][i], L-1);
}
return ans;
}
}
int main() {
int T;scanf("%d",&T);
int cas = 0;
while(T--){
memset(vis, 0, sizeof vis);
memset(in, 0, sizeof in);
AC::init();
scanf("%d",&k);
for(int i=1;i<=k;i++){
scanf("%s", s[i]+1);
AC::insert(s[i]);
}
scanf("%d",&n);
for(int i=1;i<=n;i++){
char op[10];scanf("%s", op);
scanf("%lf", &p[op[0]]);
in[op[0]] = 1;
}
AC::build();
scanf("%d",&L);
printf("Case #%d: %.6f
", ++cas, AC::get(0, L));
}
return 0;
}
3. Matrix Matcher
题意:给出一个(n*m)的字符矩阵(T),你的任务是找出给定的(x*y) 的字符矩阵(P) 出现了多少次。
分析:将 (P) 的每一行加入到AC自动机中,然后对于(T) 的每一行,去自动机中暴力匹配,如果找到了 (T) 的第 (row) 行的第 (i) 个位置匹配到了 (P) 第 (j)行字符的最后一个位置,说明在 (T[row][i-y+1]) 开始的位置可以匹配 P 的 (j) 行。这样,以 (T[row-j+1][i-y+1]) 为左上角的,大小为(x*y) 的子矩阵中,新增加了一行可以与 (P) 匹配,这个信息用(match[i][j]) 来存放,每次遇到使其加一。
最后跑一遍,满足(match[i][j] = x) 的那些((i,j)) 就是一个二位匹配点。
const int N = 100000 + 5;
int n, m, x, y;
char s[1010][1010], t[110][110];
int match[1010][1010];
namespace AC{
int tr[N][26],tot;
int e[N],fail[N];
int cnt[N];
vector<int> ends[N];
queue<int> q;
void init(){
memset(match, 0, sizeof match);
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = cnt[i] = 0;
ends[i].clear();
}
tot = 0;
}
void insert(char *s, int pos){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
e[u] ++;
ends[u].push_back(pos); // 将行序号加到对应结点末尾
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
for(int i=0;i<26;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];
}
}
}
void solve(){
for(int row=1;row<=n;row++){
int u = 0;
for(int i = 1; i <= m; i++){
u = tr[u][s[row][i] - 'a'];
//这里并不需要跳fail,因为这些模式串的长度都一定
for(auto pos : ends[u]){
match[row - pos + 1][i - y + 1] ++;
}
}
}
int res = 0;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(match[i][j] == x) res++;
printf("%d
", res);
}
}
int main() {
int T;scanf("%d",&T);
while(T--){
AC::init();
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
}
scanf("%d%d",&x,&y);
for(int i=1;i<=x;i++){
scanf("%s",t[i]+1);
AC::insert(t[i], i);
}
AC::build();
AC::solve();
}
return 0;
}
4. Detect the Virus
比较恶心的多模式串匹配,需要预处理输入,将base64转换为uchar类型
const int inf = 0x3f3f3f3f;
const int N = 100000 + 5;
char cb64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int id[200], a[100000];
char s[10000];
int t[10000];
int n;
void init(){
for(int i=0;i<64;i++){
id[cb64[i]] = i;
}
}
void pushback(int *a, int &pos, int x){
for(int i=0;i<6;i++){
a[pos++] = x >> (5-i) & 1;
}
}
int convert(char * s, int *t){ // s为base64,t为二进制
int len = strlen(s), pos = 0, top = 0;
for(int i=0;i<len;i++){
if(s[i] == '=')continue;
pushback(a,pos, id[s[i]]);
}
int x = 0;
for(int i=0;i<pos;i++){
x = x * 2 + a[i];
if((i+1) % 8 == 0){
t[top++] = x;
x = 0;
}
}
return top;
}
namespace AC{
int tr[N][256],tot;
int e[N],fail[N];
int st[N], top, v[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(int *s, int len){
int u = 0;
for(int i=0;i<len;i++){
if(!tr[u][s[i]]){
tr[u][s[i]] = ++tot;
}
u = tr[u][s[i]];
}
e[u] ++;
}
void build(){
for(int i=0;i<256;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
for(int i=0;i<256;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];
}
}
}
void solve(int *s, int len){
int res = 0, u = 0;
for(int i=0;i<len;i++){
u = tr[u][s[i]];
for(int j=u; j && !v[j]; j=fail[j]){
res += e[j];
st[++top] = j;
v[j] = 1;
}
}
for(int i=1;i<=top;i++)v[st[i]] = 0;
top = 0;
printf("%d
", res);
}
}
int main(){
init();
while(~scanf("%d",&n)){
AC::init();
for(int i=1;i<=n;i++){
scanf("%s",s);
int len = convert(s, t);
AC::insert(t, len);
}
AC::build();
scanf("%d",&n);
while(n--){
scanf("%s",s);
int len = convert(s, t);
AC::solve(t, len);
}
puts("");
}
return 0;
}
5. [P2414 NOI2011]阿狸的打字机
不难想到每个字符串结尾结点在fail树上的子孙代表的含义,对于一个询问((x,y)) ,要求的就是 x 号字符串的末尾结点的子树中,有多少个结点可以匹配到 y 中。
暴力的把所以标记打到fail树上?N个字符串,每个字符串长度不固定,有可能都很长,显然不行。每次对单个询问处理貌似都不太合适,可以把询问离线,集中处理询问。
先将fail树构建dfs序列,那么询问某个结点的子树情况可以转换为一个序列问题,接下来可以dfs扫描原Trie树(不是AC自动机构建出来的字典图),每次扫描到一个结点时,若从根节点到该结点的路径表示为询问中的 y,那么扫描它的所有询问 x,在fail树dfs序上找答案即可。
#define mk make_pair
const int N = 100000 + 5;
char s[N];
int n, m;
vector<pair<int,int>> query[N];
int c[N], res[N], pos[N];
void add(int x, int y){
for(;x<N;x+=x&-x) c[x]+=y;
}
int ask(int x){
int res = 0;
for(;x;x-=x&-x) res+=c[x];
return res;
}
namespace AC{
int tr[N][26],tot, trie[N][26];
int e[N], fail[N], fa[N];
int u;
int dfn[N], sz[N], cnt;
vector<int> ends[N];
vector<int> v[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
u = 0;
}
void insert(char c){
if(!tr[u][c-'a'])tr[u][c-'a'] = ++tot,fa[tot] = u;
u = tr[u][c-'a'];
}
void setend(int id){
e[u] ++;
ends[u].push_back(id);
pos[id] = u;
}
void getup(){
if(u != 0)
u = fa[u];
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
e[u] ++;
}
void build(){
for(int i=0;i<=tot;i++) memcpy(trie[i], tr[i], sizeof tr[i]);//保留原Trie树
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
if(u) v[fail[u]].push_back(u);//构建fail树
for(int i=0;i<26;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];
}
}
}
void dfs(int u){//构建fail树dfs序列
dfn[u] = ++cnt;
sz[u] = 1;
for(auto x : v[u]){
dfs(x);
sz[u] += sz[x];
}
}
void solve(int u){
add(dfn[u], 1);
for(auto y : ends[u]){
for(auto t : query[y]){
int x = t.first;
int id = t.second;
int dfnid = dfn[pos[x]];
res[id] = ask(dfnid + sz[pos[x]] - 1) - ask(dfnid-1);
}
}
for(int i=0;i<26;i++){
if(trie[u][i])solve(trie[u][i]);
}
add(dfn[u], -1);
}
}
int main() {
scanf("%s",s);
AC::init();
int len = strlen(s);
n = 0;
for(int i=0;i<len;i++){
if(s[i] == 'B')AC::getup();
else if(s[i] == 'P'){
AC::setend(++n);
}else AC::insert(s[i]);
}
AC::build();
AC::dfs(0);
scanf("%d",&m);
for(int i=1;i<=m;i++){
int x,y;scanf("%d%d",&x,&y);
query[y].push_back(mk(x, i));//离线询问
}
AC::solve(0);
for(int i=1;i<=m;i++) printf("%d
", res[i]);
return 0;
}
6. DNA repair
AC自动机上面DP即可,注意要记忆化以下
const int N = 1000 + 5;
int n;
char s[N];
int xid[26];
namespace AC{
int tr[N][4],tot;
int e[N],fail[N];
int vis[N][N], d[N][N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(vis[i], 0, sizeof vis[i]);
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
int c = xid[s[i] - 'A'];
if(!tr[u][c]){
tr[u][c] = ++tot;
}
u = tr[u][c];
}
e[u] ++;
}
void build(){
for(int i=0;i<4;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] |= e[fail[u]];
for(int i=0;i<4;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];
}
}
}
int get(int u, int pos, int len){
if(e[u]) return inf;//这个要放在最前面,u肯定是合法结点(不会超过tot),由于我第一次写好没放最前面,debug好久
if(pos == len + 1) return 0;
if(vis[u][pos]) return d[u][pos];
vis[u][pos] = 1;
int &res = d[u][pos];
res = inf;
int c = xid[s[pos] - 'A'];
for(int i=0;i<4;i++){
res = min(res, (i != c) + get(tr[u][i], pos+1, len));
}
return res;
}
}
int main() {
xid['A' - 'A'] = 0;
xid['G' - 'A'] = 1;
xid['C' - 'A'] = 2;
xid['T' - 'A'] = 3;
int cas = 0;
while(scanf("%d",&n) != EOF){
if(n == 0) break;
AC::init();
for(int i=1;i<=n;i++){
scanf("%s",s+1);
AC::insert(s);
}
AC::build();
scanf("%s",s+1);
int res = AC::get(0, 1, strlen(s+1));
if(res == inf) res = -1;
printf("Case %d: %d
",++cas, res);
}
return 0;
}
7. Ring
同样也是AC自动机上面DP,其实很好做的题,肯定是由于出题人懒得写spj导致一堆特殊条件
(d[u][i]) 表示从 (u) 结点出发,走 (i) 的长度,最多可以匹配多少价值,(p[u][i]) 表示路径。
先求出(d[0][n]), 然后找一个最小的 i , 使得出(d[0][i] == d[0][n]),然后按照路径输出。
由于枚举时是按照字典序从小到大的,所以这里直接输出即可
const int N = 2000 + 5;
const int M = 55;
char s[N];
int n, m, pos[N];
namespace AC{
int tr[N][26],tot;
int e[N],fail[N];
int vis[N][M], d[N][M], p[N][M];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(vis[i], 0, sizeof vis[i]);
memset(tr[i],0,sizeof tr[i]);
memset(p, 0, sizeof p);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s, int id){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
int c = s[i] - 'a';
if(!tr[u][c]){
tr[u][c] = ++tot;
}
u = tr[u][c];
}
pos[id] = u;
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] += e[fail[u]];//权值累加
for(int i=0;i<26;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];
}
}
}
int get(int u, int n){ // 从 u 开始 n 长度
if(n == 0) return 0;
if(vis[u][n]) return d[u][n]; // 记忆化搜索
vis[u][n] = 1;
int &ans = d[u][n];
ans = 0;
for(int i=0;i<26;i++){
int c = tr[u][i];
int now = e[c] + get(c, n - 1);
if(now > ans){ // 必须为大于号,因为答案要求字典序最小
ans = now;
p[u][n] = i;
}
}
return ans;
}
void print(int u, int n){
if(n == 0) return;
int c = p[u][n];
printf("%c", char('a'+c));
print(tr[u][c], n-1);
}
}
int main() {
int T;
scanf("%d",&T);
while(T--){
AC::init();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%s",s+1);
AC::insert(s, i);
}
for(int i=1;i<=m;i++){
scanf("%d", &AC::e[pos[i]]);
}
AC::build();
AC::get(0, n);
for(int i=0;i<=n;i++){
if(AC::get(0, i) == AC::get(0, n)){ //这里一定要用这种方式访问dp值!!!,由于是记忆化搜索所以再调用函数之前不一定会有d[0][i],被这里坑了
n = i;
break;
}
}
AC::print(0,n);
puts("");
}
return 0;
}
8. Searching the String
暴力跳(fail)进行统计。每次跳到一个结点$ j$,表示该结点表示的子串在 (s) 中出现了一次,对于第一类询问,直接++,对于第二类询问,需要利用上一次该位置在 (s) 中出现的位置(last),如果 (i - last >= len(j)),则表示两次出现在 (s) 中不交叉,则答案++.
因为本身模式串的长度最长才6,暴力跳fail几乎对时间没有影响。但如果还想优化,可以这样考虑,每次暴力跳fail的结果不一定会对计算结果有用,因为有一些结点并不是末尾结点,为了防止跳到这些没有用的结点,再用一个(pre) 数组来记录,在(bfs) 的过程中可以这么计算:
if(e[fail[u]]) pre[u] = fail[u];
else pre[u] = pre[fail[u]];
实际优化不是很明显:
const int N = 600000 + 5;
const int M = 100010;
int n;
char s[M], t[10];
int tp[M], res[N][2], last[N], dep[N], pos[M];
namespace AC{
int tr[N][26],tot;
int e[N],fail[N], pre[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
res[i][0] = res[i][1] = 0;
fail[i] = e[i] = 0;
last[i] = 0;
}
tot = 0;
}
void insert(char *s, int id){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
e[u] ++;
dep[u] = len;
pos[id] = u;
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
if(e[fail[u]]) pre[u] = fail[u];
else pre[u] = pre[fail[u]];
for(int i=0;i<26;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];
}
}
}
void get(){
int len = strlen(s+1);
int u = 0;
for(int i=1;i<=len;i++){
int c = s[i] - 'a';
u = tr[u][c];
for(int j = u; j; j = pre[j]){
if(e[j]){
res[j][0] ++;
if(i - last[j] >= dep[j]){
last[j] = i;
res[j][1] ++;
}
}
}
}
}
}
int main() {
int cas = 0;
while(~scanf("%s",s+1)){
AC::init();
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%s",&tp[i], t+1);
AC::insert(t, i);
}
AC::build();
AC::get();
printf("Case %d
", ++cas);
for(int i=1;i<=n;i++){
printf("%d
", res[pos[i]][tp[i]]);
}
puts("");
}
return 0;
}
9. Frequency of String
http://codeforces.com/problemset/problem/963/D
给出一个字符串S, n次询问,每次询问给出正整数k和字符串m, 要求字符串T的最小长度,满足T是S的子串,且m在T中出现了至少K次。
首先想一下暴力做法:将所有询问串构建AC自动机,然后在上面跑S,若在(s[i]) 匹配到了一个串,那么就给这个串添加一个 i 位置为结尾的出现。最后进行统计即可(连续 k 次出现的最小长度)。
每次匹配到一个位置几乎都要暴力跳 fail,因为这个题目中多模式串的长度有长有短,很容易出现某个串是另外一个串的后缀这种情况,所以很容易T。但根据上一道题目中提到的小优化进行优化呢?每次只给对答案有用的结点添加出现位置,那最终添加的次数就是每个询问串在 s 中的所有出现位置。似乎还是会有很多。
注意到,题目中说明了每个询问串都不一样。对于长度一样的询问串,在S中最多有(|S|) 次出现,而对于总长度为(sum|m|) 的多模式串来讲,(|m|) 最多有(sqrt{sum|m|}) 种,也就是将(sum|m|) 最多分成(sqrt{sum|m|})个不同的数字((sum = frac{n*(n+1)}{2})) (n) 差不多在(sqrt{sum}) 级别。
所以最后最多会添加(sqrt{sum|m|} imes|S|) 次位置。
const int N = 100000 + 5;
int n;
char s[N], t[N];
vector<int> pos[N];
int p[N], k[N], len[N];
namespace AC{
int tr[N][26],tot;
int e[N], fail[N], pre[N];
queue<int> q;
void insert(char *s, int id){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
e[u] ++;
p[u] = id;
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
if(e[fail[u]]) pre[u] = fail[u];
else pre[u] = pre[fail[u]];
for(int i=0;i<26;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];
}
}
}
void get(){
int len = strlen(s+1);
int u = 0;
for(int i=1;i<=len;i++){
int c = s[i] - 'a';
u = tr[u][c];
for(int j = u; j; j = pre[j]){
if(e[j]){
pos[p[j]].push_back(i);
}
}
}
}
}
int main() {
scanf("%s",s+1);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%s",&k[i], t+1);
len[i] = strlen(t+1);
AC::insert(t, i);
}
AC::build();
AC::get();
for(int i=1;i<=n;i++){
if(pos[i].size() < k[i]){
puts("-1");continue;
}
int res = inf;
for(int j = k[i] - 1; j < pos[i].size(); j++){
res = min(res, pos[i][j] - pos[i][j - k[i] + 1] + len[i]);
}
printf("%d
", res);
}
return 0;
}
10. Resource Archiver
很疑惑为什么网上有那么多错误题解?
下面这份代码建立在资源串不存在某个是某个子串的情况,再建好的Trie图中,从每个资源串末尾进行BFS,求出它到其他资源串末尾结点的最短距离(不能经过病毒串末尾结点),当然还要处理空串到其他所有资源串的距离。
然后跑一个TSP就可以了
const int N = 100000 + 5;
int n, m;
char s[N];
namespace AC{
int tr[N][2],tot;
int e[N],fail[N], vir[N], pos[20];
int dis[20][20], d[N], dp[1<<10][20];//dp[i][j]表示资源串状态为 i(二进制),走到了 j 号资源串的末尾结点
queue<int> q;
void init(){
memset(dis, -1, sizeof dis);
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = vir[i] = 0;
}
tot = 0;
}
void insert(char *s, int id, int type){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - '0']){
tr[u][s[i] - '0'] = ++tot;
}
u = tr[u][s[i] - '0'];
}
if(type == -1){
vir[u] = 1;//标记病毒
}else{
pos[id] = u;
}
}
void build(){
for(int i=0;i<2;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
vir[u] |= vir[fail[u]];//fail树上进行传递
for(int i=0;i<2;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];
}
}
}
void bfs(int s){
queue<int> q;
memset(d, -1, sizeof d);//-1表示不能访问,这里要和dis的默认一致,都为-1
d[pos[s]] = 0; // 时刻注意pos[s]表示Trie图中s号资源串的结点编号
q.push(pos[s]);
while(q.size()){
int x = q.front();q.pop();
for(int i=0;i<2;i++){
int u = tr[x][i];
if(!vir[u] && d[u] == -1){
d[u] = d[x] + 1;
q.push(u);
}
}
}
for(int i=1;i<=n;i++){
dis[s][i] = d[pos[i]];
}
}
void solve(){
memset(dp, 0x3f, sizeof dp);
dp[0][0] = 0;
for(int i=0; i<(1<<n); i++){
for(int j=0; j<=n; j++){ //j要从0开始,
for(int k = 1; k <= n; k++){ //k从1开始就行,0代表空串
if(dis[j][k] == -1)continue;
if(i >> (k-1) & 1) continue;
int &res = dp[i|(1<<(k-1))][k];
res = min(res, dp[i][j] + dis[j][k]);
}
}
}
int res = inf;
for(int i=1;i<=n;i++)
res = min(res, dp[(1<<n)-1][i]);
printf("%d
", res);
}
}
int main() {
while(~scanf("%d%d",&n,&m)){
if(n == 0 && m == 0){
break;
}
AC::init();
for(int i=1;i<=n;i++){
scanf("%s",s+1);
AC::insert(s, i, i);
}
for(int i=1;i<=m;i++){
scanf("%s",s+1);
AC::insert(s, -1, -1);
}
AC::build();
AC::bfs(0);
for(int i=1;i<=n;i++) AC::bfs(i);
AC::solve();
}
return 0;
}
11. 小明系列故事——女友的考验
自动机上面DP,和前面的题目基本一样
const int N = 500 + 5;
const int M = 55;
int n, m, k;
int a[N];
double x[M], y[M];
double d[55][55];
namespace AC{
int tr[N][55],tot;
int e[N],fail[N];
int vis[N][55];
double res[N][55];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
memset(vis, 0, sizeof vis);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(int *s, int len){
int u = 0;
for(int i=1;i<=len;i++){
if(!tr[u][s[i]]){
tr[u][s[i]] = ++tot;
}
u = tr[u][s[i]];
}
e[u] ++;
}
void build(){
for(int i=1;i<=n;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] |= e[fail[u]];
for(int i=1;i<=n;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];
}
}
}
double get(int u, int pos){
if(e[u]) return 1e15;
if(pos == n) return 0;
if(vis[u][pos]) return res[u][pos];
vis[u][pos] = 1;
double &ans = res[u][pos];
ans = 1e15;
for(int i=pos+1;i<=n;i++){
ans = min(ans, d[pos][i] + get(tr[u][i], i));
}
return ans;
}
}
double S(double x){ return x * x;}
int main() {
while(~scanf("%d%d",&n,&m)){
if(n == 0 && m == 0) break;
AC::init();
for(int i=1;i<=n;i++){
scanf("%lf%lf",&x[i], &y[i]);//一开始就要输入double,因为下面计算距离时,有可能爆LL
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
d[i][j] = sqrt(S(x[i]-x[j]) + S(y[i]-y[j]));
}
}
for(int i=1;i<=m;i++){
scanf("%d",&k);
for(int j=1;j<=k;j++)scanf("%d",&a[j]);
AC::insert(a, k);
}
AC::build();
double res = AC::get(AC::tr[0][1], 1);
if(res == 1e15)puts("Can not be reached!");
else
printf("%.2f
", res);
}
return 0;
}
12. Wireless Password
题意:
给出(m)个长度小于等于(10)的字符串,然后求长度为(n(nle 25))的字符串,要求其最少包含(m)个字符串中的(k)个(可以有覆盖部分),问这样的字符串有多少个。
分析:
d[i][j][k]
表示长度为 i,在 j 结点,包含字符串状态为 k 的种类数
考虑 AC自动机上 j 结点的转移,u = tr[j][c]
, e[u]
表示u结点的匹配集合,则有:
d[i+1][u][k|e[u]] += d[i][j][k]
const int N = 100 + 5;
const int mod = 20090717;
int n, m, k;
char s[N];
int d[26][N][1<<10], cnt[1<<10];
namespace AC{
int tr[N][26],tot;
int e[N],fail[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s, int id){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
if(!tr[u][s[i] - 'a']){
tr[u][s[i] - 'a'] = ++tot;
}
u = tr[u][s[i] - 'a'];
}
e[u] |= 1 << (id-1);
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] |= e[fail[u]];
for(int i=0;i<26;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];
}
}
}
void solve(){
memset(d, 0, sizeof d);
d[0][0][0] = 1;
//25 * 100 * 1024 * 26
for(int i=0;i<n;i++){
for(int j=0;j<=tot;j++){
for(int k=0;k<(1<<m);k++){
//很强大的优化
if(d[i][j][k] == 0) continue;
for(int c = 0; c < 26; c++){
int u = tr[j][c];
int &res = d[i+1][u][k|e[u]];
res = (res + d[i][j][k]) % mod;
}
}
}
}
int res = 0;
for(int i=0;i<(1<<m);i++){
if(cnt[i] < k) continue;
for(int j=0;j<=tot;j++){
res = (res + d[n][j][i]) % mod;
}
}
printf("%d
", res);
}
}
int main() {
for(int i=0;i<(1<<10);i++){
for(int j=0;j<10;j++){
if(i >> j & 1) cnt[i]++;
}
}
while(~scanf("%d%d%d",&n,&m,&k)){
if(n == 0 && m == 0 && k == 0) break;
AC::init();
for(int i=1;i<=m;i++){
scanf("%s",s+1);
AC::insert(s, i);
}
AC::build();
AC::solve();
}
return 0;
}
13.Lost‘s revenge
题意:
DNA序列,仅包含"ACGT"四种字符,给 (N(Nle 50)) 个串,每个串长度不超过10,最后给出一个字符串S((|S| le 40)), 求将 S 重新排列之后,最多能够包含N个串中的多少个?(允许覆盖)
分析:
重组串长度不超过40,字符种类数为4,如果用d[i][j][k][l][m]
来表示匹配到 i 号结点时,有 j 个 ‘A‘, k 个 ‘C‘, l 个 ‘G‘, m 个 ‘T‘时,最多包含几个串
枚举 i 的转移,u=tr[i][c]
, 若 c 表示 ‘A‘, 则 d[u][j+1][k][l][m] = max(d[u][j+1][k][l][m], d[i][j][k][l][m] + e[u])
const int N = 500 + 5;
int n;
char s[N];
int f[4];
inline int xid(char ch) {
if(ch == 'A') return 0;
if(ch == 'C') return 1;
if(ch == 'G') return 2;
if(ch == 'T') return 3;
}
namespace AC{
int tr[N][4],tot;
int e[N],fail[N];
queue<int> q;
vector<vector<vector<vector<int>>>> d[N];
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
int c = xid(s[i]);
if(!tr[u][c]){
tr[u][c] = ++tot;
}
u = tr[u][c];
}
e[u]++;
}
void build(){
for(int i=0;i<4;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] += e[fail[u]];
for(int i=0;i<4;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];
}
}
}
void solve(){
memset(f, 0, sizeof f);
int len = strlen(s+1);
for(int i=1;i<=len;i++){
f[xid(s[i])] ++;
}
for(int i = 0; i <= tot; i++){
d[i].clear();
d[i].resize(f[0]+1);
for(int j = 0; j <= f[0]; j++){
d[i][j].resize(f[1] + 1);
for(int k = 0; k <= f[1]; k++){
d[i][j][k].resize(f[2]+1);
for(int l = 0; l <= f[2]; l++){
d[i][j][k][l].resize(f[3]+1, -1);
}
}
}
}
d[0][0][0][0][0] = 0;
int res = 0;
for(int num = 0; num <= len; num ++){
for(int i = 0; i <= tot; i++){
for(int j = 0; j <= f[0] && j <= num; j++){
for(int k = 0; k <= f[1] && j + k <= num; k++){
for(int l = 0; l <= f[2] && j + k + l <= num; l++){
int m = num - j - k - l;
if(m > f[3] || d[i][j][k][l][m] == -1) continue;
if(j < f[0]) d[tr[i][0]][j+1][k][l][m] = max(d[tr[i][0]][j+1][k][l][m], d[i][j][k][l][m] + e[tr[i][0]]);
if(k < f[1]) d[tr[i][1]][j][k+1][l][m] = max(d[tr[i][1]][j][k+1][l][m], d[i][j][k][l][m] + e[tr[i][1]]);
if(l < f[2]) d[tr[i][2]][j][k][l+1][m] = max(d[tr[i][2]][j][k][l+1][m], d[i][j][k][l][m] + e[tr[i][2]]);
if(m < f[3]) d[tr[i][3]][j][k][l][m+1] = max(d[tr[i][3]][j][k][l][m+1], d[i][j][k][l][m] + e[tr[i][3]]);
if(num == len);
res = max(res, d[i][j][k][l][m]);
}
}
}
}
}
printf("%d
", res);
}
}
int main() {
int cas = 0;
while(~scanf("%d",&n)){
if(n == 0) break;
AC::init();
for(int i=1;i<=n;i++){
scanf("%s",s+1);
AC::insert(s);
}
AC::build();
scanf("%s", s+1);
printf("Case %d: ", ++cas);
AC::solve();
}
return 0;
}
14 DNA Sequence
参考题解:https://blog.csdn.net/morgan_xww/article/details/7834801
const int N = 100 + 5;
const int mod = 100000;
int n, m;
char s[N];
struct mat{
int r, c;
ll s[N][N];
mat(int r=0,int c=0):r(r),c(c){
memset(s, 0, sizeof s);
}
};
mat operator*(const mat&a, const mat&b){
mat c = mat(a.r, b.c);
for (int i = 0; i < c.r;i++){
for (int j = 0; j < c.c;j++)
for (int k = 0; k < a.c;k++)
c.s[i][j] = (c.s[i][j] + a.s[i][k] * b.s[k][j]) % mod;
}
return c;
}
mat power(mat a,int b){
mat res = a;
b--;
for (; b;b>>=1){
if(b & 1)
res = res * a;
a = a * a;
}
return res;
}
inline int xid(char ch){
if(ch == 'A') return 0;
if(ch == 'C') return 1;
if(ch == 'G') return 2;
if(ch == 'T') return 3;
}
namespace AC{
int tr[N][4],tot;
int e[N],fail[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
int c = xid(s[i]);
if(!tr[u][c]){
tr[u][c] = ++tot;
}
u = tr[u][c];
}
e[u] ++;
}
void build(){
for(int i=0;i<4;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] += e[fail[u]];
for(int i=0;i<4;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];
}
}
}
void solve(){
mat t(tot+1, tot+1);
for(int i=0;i<=tot;i++){
if(e[i]) continue;
for(int j=0;j<4;j++){
int u = tr[i][j];
if(e[u]) continue;
t.s[i][u] ++;
}
}
t = power(t, n);
int res = 0;
for(int i=0;i<=tot;i++) res = (res + t.s[0][i]) % mod;
printf("%d
", res);
}
}
int main(){
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++){
scanf("%s", s+1);
AC::insert(s);
}
AC::build();
AC::solve();
return 0;
}
15. 考研路茫茫——单词情结
和上个题基本一样
const int N = 30 + 5;
int m;
ll n;
char s[N];
struct mat{
int r, c;
ull s[N][N];
mat(int r=0,int c=0):r(r),c(c){
memset(s, 0, sizeof s);
}
void print(){
for(int i=0;i<r;i++){
for(int j=0;j<c;j++){
printf("%llu ", s[i][j]);
}
puts("");
}
}
}one;
mat operator*(const mat&a, const mat&b){
mat c = mat(a.r, b.c);
for (int i = 0; i < c.r;i++){
for (int j = 0; j < c.c;j++)
for (int k = 0; k < a.c;k++)
c.s[i][j] = c.s[i][j] + a.s[i][k] * b.s[k][j];
}
return c;
}
mat operator+(const mat&a, const mat&b){
mat c = mat(a.r, b.c);
for (int i = 0; i < c.r;i++){
for (int j = 0; j < c.c;j++){
c.s[i][j] = a.s[i][j] + b.s[i][j];
}
}
return c;
}
mat power(mat a,ll b){
mat res = a;
b--;
for (; b;b>>=1){
if(b & 1)
res = res * a;
a = a * a;
}
return res;
}
mat getsum2(mat a, ll b){
if(b == 0) return one;
if(b == 1) return a;
if(b & 1){
return (one + power(a, (b+1)/2)) * getsum2(a, b / 2) + power(a, (b+1)/2);
}
else{
return (one + power(a, b/2)) * getsum2(a, b / 2);
}
}
ull ksm(ull a, ull b){
ull res = 1;
for(;b;b>>=1){
if(b & 1) res = res * a;
a = a * a;
}
return res;
}
ull getsum(ull a, ull b){
if(b == 0) return 1;
if(b == 1) return a;
if(b & 1){
return (ksm(a, (b+1)/2) + 1) * getsum(a, b / 2) + ksm(a, (b+1)/2);
}
else{
return (ksm(a, b/2) + 1) * getsum(a, b / 2);
}
}
namespace AC{
int tr[N][26],tot;
int e[N],fail[N];
queue<int> q;
void init(){
for(int i=0;i<=tot;i++){
memset(tr[i],0,sizeof tr[i]);
fail[i] = e[i] = 0;
}
tot = 0;
}
void insert(char *s){
int u = 0;
int len = strlen(s + 1);
for(int i=1;i<=len;i++){
int c = s[i] - 'a';
if(!tr[u][c]){
tr[u][c] = ++tot;
}
u = tr[u][c];
}
e[u] ++;
}
void build(){
for(int i=0;i<26;i++)if(tr[0][i])q.push(tr[0][i]);
while(q.size()){
int u = q.front();q.pop();
e[u] += e[fail[u]];
for(int i=0;i<26;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];
}
}
}
void solve(){
mat t(tot+1, tot+1);
one = mat(tot+1, tot+1);
for(int i=0;i<=tot;i++)one.s[i][i] = 1;
for(int i=0;i<=tot;i++){
if(e[i]) continue;
for(int j=0;j<26;j++){
int u = tr[i][j];
if(e[u]) continue;
t.s[i][u] ++;
}
}
t = getsum2(t, n);
ull res = 0;
for(int i=0;i<=tot;i++) res += t.s[0][i];
printf("%llu
", getsum(26, n) - res);
}
}
int main(){
while(~scanf("%d%lld",&m,&n)){
AC::init();
for(int i=1;i<=m;i++){
scanf("%s", s+1);
AC::insert(s);
}
AC::build();
AC::solve();
}
return 0;
}
16. Censored!
d[i][j]
表示长度为 i,到达 j 号节点的状态
if(e[u] == 0){
d[i+1][u] += d[i][j];// u = tr[j][c];
}
- 输入的字符不太正常,应该是会超过127导致出现负数
- 没取模,应该用大数
#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
#define dbg(x...) do { cout << "