新的学期
ACM依然是我的热爱
怀着一颗热爱的心去学习,去训练
周一 3.1(KMP+Manacher)
回归编程
我一位很优秀的同学说
如果你做的事情是你热爱的,那么你就不会觉得累
acm就是我热爱的,写题学算法永远不会觉得累
用热爱驱动训练,加油
现在的大致规划是
先搞好周训的内容和提高篇那本书的内容
周训学号,那本书上的题基本做完
之后再去洛谷题单刷题
【模板】KMP字符串匹配
算是复习了
自己靠原理写出来了,有几个地方注意一下
1.求next数组是b的,不是a的
2.next数组的含义是next[i]表示0~i-1这一个字串中最长的前缀=后缀的长度,注意不包括i
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
char a[N], b[N];
int Next[N], lena, lenb;
void get_next()
{
Next[0] = -1;
int i = 0, j = -1; //注意一开始i为0,j为-1 为了让Next[1] = 0 也不会存在全串的情况
while(i < lenb)
{
if(j == -1 || b[i] == b[j]) //是配b数组
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
void kmp()
{
int i = 0, j = 0;
while(i < lena)
{
if(j == -1 || a[i] == b[j])
{
i++; j++;
if(j == lenb) printf("%d\n", i - j + 1);//i-j就是匹配的初始位置,是0开始算的
} //=lenb后会重新匹配的,因为j越界了,不可能相等
else j = Next[j];
}
}
int main()
{
scanf("%s%s", a, b);
lena = strlen(a), lenb = strlen(b);
get_next();
kmp();
REP(i, 0, lenb) printf("%d ", Next[i + 1]); //Next数组的含义
puts("");
return 0;
}
P2375 [NOI2014] 动物园(kmp原理拓展)
独立做出蓝题
这题是道好题目
Next[i]可以理解为前i个字符,理解为下标也可以
首先要求前后缀相同而不重复的最长长度
我这里是求了两遍kmp
先正常的,第二遍加一个判断求出另外一个数组。直接暴力会超时,这样不超时
然后统计的时候记忆化一下防止超时就好了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
const int MOD = 1e9 + 7;
int Next[N], f[N], k[N], len;
char a[N];
void get_next1()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < len)
{
if(j == -1 || a[i] == a[j])
{
i++; j++;
Next[i] = j;
}
else j = Next[j];
}
}
void get_next2()
{
Next[0] = -1;
int i = 0, j = -1;
while(i < len)
{
if((j == -1 || a[i] == a[j]) && (j + 1) * 2 <= i + 1)
{
i++; j++;
k[i] = j;
}
else j = Next[j];
}
}
int num(int i)
{
if(f[i]) return f[i];
if(i <= 0) return 0;
return f[i] = 1 + num(Next[i]);
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
scanf("%s", a);
len = strlen(a);
get_next1();
get_next2();
int ans = 1;
memset(f, 0, sizeof(f));
_for(i, 1, len)
ans = ans * 1ll * (num(k[i]) + 1) % MOD;
printf("%d\n", ans);
}
return 0;
}
P3805 【模板】manacher算法
同样理解算法原理
求一个字符串最大的回文子串长度
暴力O(n^2)这个算法可以优化到O(n)
和kmp有点像,都是利用之前已经匹配过的信息
首先我们需要一个对称中心,因为偶回文的对称中心是空隙,所以要全部转化为奇回文
于是加入#,同时两端加入不一样的字符防止越界,这就是初始化
初始化用string会好写一些
用p[i]表示以i为对称中心的最长回文串半径
我这个写法是不包括i本身的半径
对于之前求过的回文串最长右边界为mx
如果新的对称中心在mx左侧,就可以利用前面的信息确定一部分的回文串
这时注意不能超过mx-i,所以要取min
也就是利用前面的信息赋个初值,然后开始暴力判断,优化在这个赋初值这里
判断后我们要尽可能拓展mx,是的后面的i尽可能能赋初值
然后p[i]就是原串中以i这个位置为对称中心的的回文串长度,用它来更新ans
注意p数组要开字符串长度的两倍,因为字符串会初始化
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1.1e7 + 10;
string a, str;
int p[N << 1], len;
int Manacher()
{
int id = 0, mx = 0, ans = 0;
len = str.size();
REP(i, 1, len)
{
if(i < mx) p[i] = min(p[2 * id - i], mx - i); //赋初值
else p[i] = 0;
while(str[i - p[i] - 1] == str[i + p[i] + 1]) p[i]++; //暴力拓展
if(i + p[i] > mx) //尽可能拓展mx
{
mx = i + p[i];
id = i;
}
ans = max(ans, p[i]); //用p[i]更新答案
}
return ans;
}
int main()
{
ios::sync_with_stdio(0); //加速cin,写了之后要不全部用cout,要不全部用printf
cin.tie(0);
cin >> a;
len = a.size();
str = "&"; //str为初始化后的结果
REP(i, 0, len) str += "#", str += a[i];
str += "#^";
cout << Manacher() << endl;
return 0;
}
P1659 [国家集训队]拉拉队排练(回文串问题)
用Manacher算法求出以每个点为对称中心的回文串,可以说它是一个工具
然后统计答案就行,因为Manacher算的是最长回文串,对于p[i]来说,p[i] - 2, p[i] - 4都是答案
for一遍就好,注意要用快速幂
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
const int MOD = 19930726;
string a, str;
int p[N << 1], cnt[N], n;
long long k;
int mul(int a, int b) { return 1ll * a * b % MOD; }
void Manacher()
{
int id = 0, mx = 0, len = str.size();
REP(i, 1, len)
{
if(i < mx) p[i] = min(mx - i, p[2 * id - i]);
else p[i] = 0;
while(str[i - p[i] - 1] == str[i + p[i] + 1]) p[i]++;
if(mx < i + p[i])
{
mx = i + p[i];
id = i;
}
if(p[i] & 1) cnt[p[i]]++;
}
}
int binpow(int a, int b)
{
int res = 1;
for(; b; b >>= 1)
{
if(b & 1) res = mul(res, a);
a = mul(a, a);
}
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> k >> a;
str = "%";
REP(i, 0, n) str += "#", str += a[i];
str += "#@";
Manacher();
int ans = 1, t = 0;
for(int i = n; i >= 1; i--)
if(i & 1)
{
if(cnt[i] + t <= k)
{
ans = mul(ans, binpow(i, cnt[i] + t));
k -= cnt[i] + t;
t += cnt[i];
}
else
{
ans = mul(ans, binpow(i, k));
k = 0;
break;
}
}
if(k) puts("-1");
else printf("%d\n", ans);
return 0;
}
周二 3.2(Manacher+Tire树+AC自动机)
P4555 [国家集训队]最长双回文串(Manacher+堆)
这题写了一上午,一点一点想出,很爽
写完看了题解发现我的做法和别人的都不一样哈哈哈
首先用Manacher算法求出p[i],然后再每一个回文子串的左右端点标记一下,也就是代码中的L数组和R数组
很容易想到标记时长度更长肯定更优秀,所以我取了max
然后我就想到了一个O(n^2)的算法,枚举一个回文子串的右端点,再这个点前面枚举左端点
那么长度就是两个回文子串长度和-2 * 两个回文子串公共的部分,这个拿笔画一下就好了
那么显然要优化,不然会超时
式子len1 + len2 - 2 * 公共部分
len1是固定的,主要是后面两部分要最大
我发现当i++时,只有公共部分+2,而且是所有的都加2
所有都加2表示不同的len2它们的相对大小位置是不变的
我们要最优,那就要最大的
那么我们就可以用一个优先队列来维护
这里提一下,优先队列是大根堆,sort是从小到大。我以为优先队列小根堆卡了半天
用堆维护len2,后加入的要加上2,因为公共部分变了
这样就用一个堆优化成了nlogn了,主要是观察那个式子的特性
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
string a, str;
int p[N << 1], L[N << 1], R[N << 1], len;
struct node
{
int j, l;
bool operator < (const node& rhs) const
{
return l < rhs.l;
}
};
priority_queue<node> q;
void Manacher()
{
int id = 0, mx = 0; len = str.size();
REP(i, 1, len)
{
if(i < mx) p[i] = min(mx - i, p[2 * id - i]);
else p[i] = 0;
while(str[i + p[i] + 1] == str[i - p[i] - 1]) p[i]++;
if(i + p[i] > mx)
{
mx = i + p[i];
id = i;
}
R[i + p[i]] = max(R[i + p[i]], p[i] * 2 + 1);
L[i - p[i]] = max(L[i - p[i]], p[i] * 2 + 1);
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> a;
len = a.size();
str = "@";
REP(i, 0, len) str += "#", str += a[i];
str += "#!";
Manacher();
int ans = 0, t = 4;
q.push(node{1, L[1]});
q.push(node{2, L[2] + 2});
_for(i, 2, len - 2)
{
ans = max(ans, (R[i] - 1) / 2 - 1);
if(i + 1 <= len - 2) q.push(node{i + 1, L[i + 1] + t});
t += 2;
while(!q.empty())
{
int j = q.top().j;
if(j + (L[j] - 1) / 2 > i) break;
q.pop();
}
if(!q.empty())
{
int j = q.top().j;
ans = max(ans, (R[i] + L[j] - 2 * (i - j + 1)) / 2);
}
}
printf("%d\n", ans);
return 0;
}
复习nth_element
nth_element(a, a + x, a + n)
可以把第x大(第0大, 第1大)的元素放在a[x]
x前面的数都小于等于x,后面都大于等于x
可以将排序好后a[x]的值放在a[x],前面小于等于a[x]后面大于等于a[x]
Immediate Decodability(Trie树裸题)
做了一道裸题复习了一下Trie树
顺利写出,果然懂原理还是非常重要
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1100;
int t[N][2], vis[N], cnt, ans;
char s[15];
void add(char* str)
{
int len = strlen(str), p = 0;
REP(i, 0, len)
{
int id = str[i] - '0';
if(i == len - 1 && t[p][id]) ans = 1;
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
if(vis[p]) ans = 1;
}
vis[p] = 1;
}
int main()
{
int kase = 0;
while(~scanf("%s", s))
{
memset(t, 0, sizeof(t));
memset(vis, 0, sizeof(vis));
cnt = ans = 0;
add(s);
while(scanf("%s", s) && strcmp(s, "9")) add(s);
if(ans) printf("Set %d is not immediately decodable\n", ++kase);
else printf("Set %d is immediately decodable\n", ++kase);
}
return 0;
}
P3808 【模板】AC自动机(简单版)
通过好几个小时的研究
最后静下心来慢慢理解慢慢读,终于理解了AC自动机,不容易
但是理解还不深刻,需要做题来强化
实际上就是Trie树上跑kmp
kmp中是j = next[j],是一个串中跳
但是这里的转移就是所有串中最长的next,所以会在多个串中跳来跳去
kmp中一个串会匹配多次,AC自动机也一样,所以要避免重复计算
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int t[N][26], fail[N], End[N], n, cnt = 1; //cnt = 1写在全局,不要写到add
char s[N];
void add(char* str)
{
int p = 1, len = strlen(str); //p从1开始
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
End[p]++;
}
void get_fail() //AC自动机主要过程。相当于把Trie树补全,后面查询时直接查
{
_for(i, 0, 25) t[0][i] = 1; //注意有0节点,因为涉及到父亲的fail
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]); //有则加入
int v = fail[u];
while(v && !t[v][i]) v = fail[v]; //类似求next数组中不匹配就一直J = next[j]
fail[t[u][i]] = t[v][i]; //fail[t[u][i]]
}
else t[u][i] = t[fail[u]][i]; //值肯定存在,因为更浅的Trie都补全了
} //如果失配了,那么就跳到t[fail[u]][i]
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%s", s);
add(s);
}
get_fail();
scanf("%s", s);
int len = strlen(s), p = 1, ans = 0;
REP(i, 0, len) //Trie补全后直接遍历就好
{
int id = s[i] - 'a';
p = t[p][id];
ans += End[p];
End[p] = 0; //避免重复计算,因为回跳可能像kmp跳回自己,后面又算一次
}
printf("%d\n", ans);
return 0;
}
周三 3.3 (AC自动机)
P3808 【模板】AC自动机(简单版)
发现昨天写的那个模板有问题
需要把所有后缀都统计一遍,同时防止重复
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
int t[N][26], fail[N], End[N], n, len, cnt = 1;
char s[N];
void add(char* str)
{
int p = 1; len = strlen(str);
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
End[p]++; //注意重复单词
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i]; //t[fail[u]][i]不可能为0,一定指向正确的节点
}
else t[u][i] = t[fail[u]][i]; //t[fail[u]][i]一定是指向一个确定的节点
} //补全Tried树
}
}
int main()
{
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%s", s);
add(s);
}
get_fail();
scanf("%s", s);
int p = 1, ans = 0; len = strlen(s);
REP(i, 0, len)
{
p = t[p][s[i] - 'a'];
for(int j = p; j > 1 && End[j] != -1; j = fail[j]) //遍历所有后缀
{
ans += End[j];
End[j] = -1;
}
}
printf("%d\n", ans);
return 0;
}
Hdu 2222(AC自动机裸题)
End数组写++不要写=1
觉得题目没说清楚会不会有重复的单词,重复了怎么算
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 5e5 + 10;
const int M = 1e6 + 10;
int t[N][26], fail[N], End[N], n, len, cnt;
char s[M];
void add(char* str)
{
int p = 1; len = strlen(str);
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
End[p]++;
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
}
else t[u][i] = t[fail[u]][i];
}
}
}
int main()
{
int T; scanf("%d", &T);
while(T--)
{
cnt = 1;
memset(t, 0, sizeof(t));
memset(fail, 0, sizeof(fail));
memset(End, 0, sizeof(End));
scanf("%d", &n);
_for(i, 1, n)
{
scanf("%s", s);
add(s);
}
get_fail();
scanf("%s", s);
int p = 1, ans = 0; len = strlen(s);
REP(i, 0, len)
{
p = t[p][s[i] - 'a'];
for(int j = p; j > 1 && End[j] != -1; j = fail[j])
{
ans += End[j];
End[j] = -1;
}
}
printf("%d\n", ans);
}
return 0;
}
bzoj 4327(AC自动机)
用AC自动机可以遍历所有主串与模式串匹配的部分
每个p的位置到根的这个子串就是一个匹配的部分,这个串是主串的一个子串,也是某些模式串的前缀(或整个串)
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 4e5 + 10;
const int M = 1e7 + 10;
int t[N][4], fail[N], End[N], fa[N], vis[N], size[N];
int len, cnt, n, m;
char T[M], s[M];
map<char, int> id;
void add(char* str, int i)
{
int p = 1; len = strlen(str);
REP(i, 0, len)
{
int k = id[str[i]];
if(!t[p][k]) t[p][k] = ++cnt;
fa[t[p][k]] = p;
p = t[p][k];
}
End[i] = p;
}
void get_fail()
{
_for(i, 0, 3) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 3)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
}
else t[u][i] = t[fail[u]][i];
}
}
}
int main()
{
cnt = 1;
id['E'] = 0; id['S'] = 1;
id['W'] = 2; id['N'] = 3;
scanf("%d%d%s", &n, &m, T);
_for(i, 1, m)
{
scanf("%s", s);
add(s, i);
size[i] = strlen(s);
}
get_fail();
int p = 1; len = strlen(T);
vis[1] = 1;
REP(i, 0, len)
{
p = t[p][id[T[i]]];
for(int j = p; j > 1 && !vis[j]; j = fail[j]) //遍历所有主串和模式串匹配的部分
{ //注意不要重复遍历
vis[j] = 1;
while(!vis[fa[j]]) j = fa[j], vis[j] = 1;
}
}
_for(i, 1, m)
{
int t = End[i];
if(vis[t]) printf("%d\n", size[i]);
else
{
int ans = 0;
while(!vis[fa[t]]) t = fa[t], ans++;
printf("%d\n", size[i] - ans - 1);
}
}
return 0;
}
bzoj 3940(AC自动机+栈)
之前做过一道kmp+栈,这里改成多个串就是AC自动机
思路是一样的,只不过实现方法不一样
这里有一点就是会重复遍历,容易超时
所以记忆化一下
不合法要返回-1,不要返回0,和没遍历过混淆
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e5 + 10;
int t[N][26], fail[N], End[N], k[N], d[N], f[N];
int len, n, cnt = 1;
char T[N], s[N];
void add(char* str)
{
int p = 1, h = 0; len = strlen(str);
REP(i, 0, len)
{
int id = str[i] - 'a';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
h++;
}
End[p] = 1;
d[p] = h;
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
}
else t[u][i] = t[fail[u]][i];
}
}
}
int find(int p)
{
if(p <= 1) return -1;
if(f[p]) return f[p];
if(End[p]) return f[p] = p;
return f[p] = find(fail[p]);
}
int main()
{
scanf("%s%d", T, &n);
_for(i, 1, n)
{
scanf("%s", s);
add(s);
}
get_fail();
int p = 1, top = 0; len = strlen(T);
REP(i, 0, len)
{
s[++top] = T[i];
p = t[p][T[i] - 'a'];
k[top] = p;
int t = find(p);
if(t > 1) top -= d[t], p = k[top];
}
_for(i, 1, top) printf("%c", s[i]); puts("");
return 0;
}
周四 3.4(AC自动机)
今天能抽出的时间稍微少了一些
加上笔记本忘记充电智障了耽误了时间
不过我真的一天不碰键盘就手痒
晨跑习惯了,今天下雨没跑身体就不爽
保持下去
「一本通 2.4 练习 3」单词(AC自动机)
我发现AC自动机好多省选题,基本都是紫题
其实还好啦,算法本身理解比较难,但是理解后做题也就还好吧
这题的话一开始感觉比较奇怪,没有T串
首先建Trie ac自动机是肯定要的,然后咋整呢
我想到把论文当作T串,去匹配。AC自动机可以遍历所有匹配的情况,所以就用一个数组存一下匹配成功的次数
然后做完发现答案错的,原来是单词和单词之间不能连在一起
所以需要一个单词一个单词当作T串取匹配,每次匹配都重新开始
以为大概率超时,然后发现只超时了一个点
想了想,每次其实遍历很快,因为都重新开始了
超时地一个点应该是专门卡的
然后我就优化了一下,就是重复的单词合并在一起,就过了
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 1e6 + 10;
const int M = 210;
int t[N][26], fail[N], End[M], num[N], v[N];
int n, len, cnt = 1;
string s[M];
void add(string s, int j)
{
int p = 1; len = s.size();
REP(i, 0, len)
{
int id = s[i] - 'a';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
End[j] = p;
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
}
else t[u][i] = t[fail[u]][i];
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
_for(i, 1, n)
{
cin >> s[i];
add(s[i], i);
}
get_fail();
_for(i, 1, n)
{
if(v[i] == -1) continue;
v[i] = 1;
_for(j, i + 1, n)
if(s[i] == s[j])
v[i]++, v[j] = -1;
}
_for(k, 1, n)
{
if(v[k] == -1) continue;
int p = 1; len = s[k].size();
REP(i, 0, len)
{
p = t[p][s[k][i] - 'a'];
for(int j = p; j > 1; j = fail[j]) num[j] += v[k];
}
}
_for(i, 1, n) cout << num[End[i]] << endl;
return 0;
}
周六 3.6(AC自动机)
昨天没记录是因为写一道题想了很久没想出来
今天又想了很久,最后看了题解
P2322 [HNOI2006]最短母串问题(AC自动机+状态压缩+bfs求最短路)
这题还是很精彩的
我自己做的时候看到n=12 想到的是搜索,但又有点虚怕会超时
就是搜索枚举所有情况,一些记录注意回溯时恢复现场
最后果然60分超时了
然后就没思路了
看题解发现用到状态压缩的方法
这个知识点我太不熟练了,状压dp我之后会好好搞它一遍
所以做题要独立思考,思考了足够长时间就可以看题解,可能是有些知识点或者算法你不知道
n <= 12也暗示了状态压缩
首先很容易想到就是求一条最短路径使得遍历了Trie树上所有的字符串的终点
那么可以把目前经历了哪些字符串用二进制存一个状态
在AC自动机时可以预处理,把当前节点的fail的状态也合并进来
然后就开始bfs,按照字典序拓展,最先达到经历了所有字符串状态的就是答案
bfs的时候记得去掉重复状态,后面重复的状态肯定不是答案,时间优化
然后记录答案的时候可以用ans 和fa数组来存,不用存字符串消耗空间
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 610;
int t[N][26], fail[N], state[N], fa[N * (1 << 12)], vis[N][1 << 12];
int len, n, cnt = 1;
struct node{ int u, s; };
vector<char> ans;
string s;
void add(string str, int i)
{
int p = 1; len = str.size();
REP(i, 0, len)
{
int id = str[i] - 'A';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
state[p] |= 1 << (i - 1);
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
state[t[u][i]] |= state[fail[t[u][i]]];
}
else t[u][i] = t[fail[u]][i];
}
}
}
void print(int x)
{
if(x == 0) return;
print(fa[x]);
cout << ans[x];
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
_for(i, 1, n)
{
cin >> s;
add(s, i);
}
get_fail();
queue<node> q;
q.push(node{1, 0});
int id = 0;
ans.push_back(' ');
while(!q.empty())
{
node x = q.front(); q.pop();
int u = x.u, s = x.s;
if(s == (1 << n) - 1)
{
print(id);
break;
}
_for(i, 0, 25)
if(!vis[t[u][i]][s | state[t[u][i]]])
{
vis[t[u][i]][s | state[t[u][i]]] = 1;
q.push(node{t[u][i], s | state[t[u][i]]});
ans.push_back(i + 'A');
fa[ans.size() - 1] = id;
}
id++;
}
return 0;
}
周日 3.7 (AC自动机)
昨晚打了第一场积分赛
我太想拿好成绩,从而太紧张了,根本不能静下心来想问题,导致发挥失常
一直犯一些低级错误
我虽然训练了挺久,但是比赛经验还是太少
今晚又有一场
要能静下心来想问题,合理规划时间
比赛时的心理素质也是非常重要的一个方面
「一本通 2.4 练习 5」病毒(AC自动机+dfs+避免重复遍历)
这题显然就是在Trie树上跑,在不经过字符串终点的情况看是否能循环
Trie树上走过一个节点时不仅包含了当前根到此节点的字符串,还包含一些其他字符串,这些字符串是此节点字符串的后缀,也就是fail
一般会在初始化的时候延续fail的某些信息,或者在统计的时候遍历fail
这里是在初始化的时候延续
做dfs就好
需要注意的是,一般题目中dfs不会重复遍历一个节点,但是这道题会,在Trie树上跳可能又跳到同一个节点
所以要判重,不要重复遍历。判重就回溯的时候处理一下就好。
不然会T掉2个点
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 3e4 + 10;
int t[N][2], fail[N], vis[N], End[N];
int len, n, cnt = 1;
string s;
void add(string str)
{
int p = 1; len = str.size();
REP(i, 0, len)
{
int id = str[i] - '0';
if(!t[p][id]) t[p][id] = ++cnt;
p = t[p][id];
}
End[p] = 1;
}
void get_fail()
{
_for(i, 0, 1) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 1)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
End[t[u][i]] |= End[fail[t[u][i]]]; //延续信息
}
else t[u][i] = t[fail[u]][i];
}
}
}
bool dfs(int u)
{
_for(i, 0, 1)
{
if(End[t[u][i]] || vis[t[u][i]] == 2) continue;
if(vis[t[u][i]] == 1) return true;
vis[t[u][i]] = 1;
if(dfs(t[u][i])) return true;
vis[t[u][i]] = 2; //避免重复遍历
}
return false;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
_for(i, 1, n) cin >> s, add(s);
get_fail();
if(dfs(1)) puts("TAK");
else puts("NIE");
return 0;
}
P3796 【模板】AC自动机(加强版)(AC自动机+树形dp)
这题就是要统计每个模式串出现的次数
暴力跳肯定超时,考虑怎么优化
首先那些对答案不产生贡献的点可以略去,所以要修改一下fail数组
其次先标记,然后做一遍树形dp
我自己的做法就是按照深度,从最深的开始,每一个高度的都往它的fail加一次,这样统计是正确的,复杂度也是O(n)
有点像倒序的bfs
本质上应该和树形dp差不多
#include<bits/stdc++.h>
#define REP(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;
const int N = 10500 + 10;
int t[N][26], fail[N], End[N], num[N], len, n, cnt = 1;
string s[160], T;
vector<int> ve[80];
void add(string str, int i)
{
int p = 1, h = 0; len = str.size();
REP(i, 0, len)
{
h++;
int id = str[i] - 'a';
if(!t[p][id])
{
t[p][id] = ++cnt;
ve[h].push_back(t[p][id]);
}
p = t[p][id];
}
End[i] = p;
}
void get_fail()
{
_for(i, 0, 25) t[0][i] = 1;
fail[1] = 0;
queue<int> q;
q.push(1);
while(!q.empty())
{
int u = q.front(); q.pop();
_for(i, 0, 25)
{
if(t[u][i])
{
q.push(t[u][i]);
fail[t[u][i]] = t[fail[u]][i];
}
else t[u][i] = t[fail[u]][i];
}
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
while(cin >> n && n)
{
cnt = 1;
memset(t, 0, sizeof(t));
memset(num, 0, sizeof(num));
_for(i, 1, 75) ve[i].clear();
_for(i, 1, n) cin >> s[i], add(s[i], i);
get_fail();
cin >> T;
len = T.size();
int p = 1;
REP(i, 0, len)
{
p = t[p][T[i] - 'a'];
num[p]++;
}
for(int i = 75; i >= 1; i--)
for(auto u: ve[i])
num[fail[u]] += num[u];
int mx = 0;
_for(i, 1, n)
mx = max(mx, num[End[i]]);
cout << mx << endl;
_for(i, 1, n)
if(num[End[i]] == mx)
cout << s[i] << endl;
}
return 0;
}