【kuangbin带你飞】专题十七AC自动机
hdu2222 - Keywords Search
思路
AC自动机的模板题
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 50 * 10000 + 10, M = 1e6 + 10;
int tr[N][26], ne[N], cnt[N], idx;
char s[M];
void insert(char str[]) {
int p = 0;
for(int i = 0; str[i]; i++) {
int id = str[i] - 'a';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
cnt[idx] = ne[idx] = 0;
}
p = tr[p][id];
}
cnt[p]++;
}
int q[N];
void build() {
int l = 1, r = 0;
for(int i = 0; i < 26; i++) {
if(tr[0][i]) q[++r] = tr[0][i];
}
while(l <= r) {
int u = q[l++];
for(int i = 0; i < 26; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
q[++r] = c;
}
}
}
}
void solve() {
int n;
scanf("%d", &n);
memset(tr[0], 0, sizeof tr[0]);
idx = 0, cnt[0] = ne[0] = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s);
insert(s);
}
build();
scanf("%s", s + 1);
int len = strlen(s + 1);
int res = 0;
for(int i = 1, j = 0; i <= len; i++) {
int id = s[i] - 'a';
j = tr[j][id];
int p = j;
while(p && ~cnt[p]) {
res += cnt[p];
cnt[p] = -1;
p = ne[p];
}
}
printf("%d\n", res);
}
int main() {
// freopen("in.txt", "r", stdin);
int t; cin >> t; while(t--)
solve();
return 0;
}
hdu2896 - 病毒侵袭
思路
还是AC自动机模板题,只是在小范围上处理比较复杂,对于每个串单独判断一遍,同时用vector记录一下跑到哪些病毒串即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
int tr[N][130], ne[N], idx, cnt[N];
bool vis[N];
char str[N];
void insert(char s[], int id) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = s[i];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
cnt[idx] = 0;
ne[idx] = 0;
}
p = tr[p][id];
}
cnt[p] = id;
}
int q[N];
void build() {
int l = 1, r = 0;
for(int i = 0; i <= 128; i++) {
if(tr[0][i]) q[++r] = tr[0][i];
}
while(l <= r) {
int u = q[l++];
for(int i = 0; i <= 128; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
q[++r] = c;
}
}
}
}
void solve() {
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%s", str);
insert(str, i);
}
build();
int m; scanf("%d", &m);
int t = 0;
for(int id = 1; id <= m; id++) {
scanf("%s", str + 1);
vector<int> res;
memset(vis, 0, sizeof vis);
for(int i = 1, j = 0; str[i]; i++) {
int id = str[i];
j = tr[j][id];
int p = j;
while(p && !vis[p]) {
if(cnt[p]) res.push_back(cnt[p]);
vis[p] = true;
p = ne[p];
}
}
if(res.size() > 0) {
t++;
sort(res.begin(), res.end());
printf("web %d:", id);
for(int i = 0; i < res.size(); i++) {
printf(" %d", res[i]);
}
puts("");
}
}
printf("total: %d\n", t);
}
int main() {
// freopen("in.txt", "r", stdin);
solve();
return 0;
}
hdu3065 - 病毒侵袭持续中
思路
模板题,多开一个cnt数组记录每个病毒串出现几次输出即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 5e4 + 10, M = 2e6 + 10;
int tr[N][26], ne[N], cnt[N], idx;
char str[M];
char s[1010][55];
int res[1010], n;
void insert(char s[], int id) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = s[i] - 'A';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
cnt[idx] = ne[idx] = 0;
}
p = tr[p][id];
}
cnt[p] = id;
}
queue<int> q;
void build() {
for(int i = 0; i < 26; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
q.push(c);
}
}
}
}
void solve() {
// int n;
// scanf("%d", &n);
idx = 0;
memset(tr[0], 0, sizeof tr[0]);
ne[0] = cnt[0] = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s[i]);
insert(s[i], i);
res[i] = 0;
}
build();
scanf("%s", str);
for(int i = 0, j = 0; str[i]; i++) {
if(str[i] < 'A' || str[i] > 'Z') {
j = 0;
continue;
}
int id = str[i] - 'A';
j = tr[j][id];
int p = j;
while(p) {
if(cnt[p]) res[cnt[p]]++;
p = ne[p];
}
}
for(int i = 1; i <= n; i++) {
if(res[i]) {
printf("%s: %d\n", s[i], res[i]);
}
}
}
int main() {
// freopen("in.txt", "r", stdin);
while(~scanf("%d", &n)) {
solve();
}
// solve();
return 0;
}
poj2778-DNA Sequence
思路
一开始想着肯定是一个经典的ac自动机+dp题,但是数据范围很大,就需要考虑别的办法。
通过ac自动机建立一个trie图,用vis数组标记一下表示不能到达哪些点。然后通过建好的ac自动机建立矩阵快速幂,矩阵快速幂m[i][j]表示在trie图上从i点到j的方案数。离散数学中可以知道在一个图上走了n次,就相当于是所建矩阵的n次幂。拿矩阵快速幂跑n次以后,计算m[0][i]表示从起点0到tire图上i点的所有方案数,累加一下即可。
代码
#include<iostream>
#include<map>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
typedef long long LL;
#define int LL
typedef pair<int, int> PII;
#define Martix Matrix
#define gcd __gcd
#define maxn 110
const int mod = 100000;
const int N = 110 + 10;
int tr[N][5], idx, ne[N], n, k;
char s[N];
bool vis[N];
map<char, int> mp;
struct Matrix {
int m[N][N];
Matrix() {
memset(m, 0, sizeof(m)); // 初始化矩阵
}
};
Matrix Multi(Matrix a, Matrix b) // 矩阵乘法
{
Matrix res;
for(int i = 0; i <= idx; i++) {
for(int j = 0; j <= idx; j++) {
for(int k = 0; k <= idx; k++) {
res.m[i][j] = (res.m[i][j] + a.m[i][k] * b.m[k][j]) % mod;
}
}
}
return res;
}
Martix fastm(Martix a, int n) // 矩阵快速幂
{
Martix res;
for(int i = 0; i <= idx; i++) { // 等同于快速幂的res = 1的操作
res.m[i][i] = 1;
}
while(n) {
if(n & 1) res = Multi(res, a);
a = Multi(a, a);
n >>= 1;
}
return res;
}
void insert(char str[]) {
int p = 0;
for(int i = 0; str[i]; i++) {
int id = mp[str[i]];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
vis[idx] = false;
ne[idx] = 0;
}
p = tr[p][id];
}
vis[p] = true;
}
void build() {
queue<int> q;
for(int i = 0; i < 4; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 4; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
vis[0] = false, ne[0] = 0; idx = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s);
insert(s);
}
build();
Matrix x;
for(int i = 0; i <= idx; i++) {
if(vis[i]) continue;
for(int j = 0; j < 4; j++) {
if(!vis[tr[i][j]]) {
x.m[i][tr[i][j]]++;
}
}
}
x = fastm(x, k);
LL res = 0;
for(int i = 0; i <= idx; i++) {
res = (res + x.m[0][i]) % mod;
}
printf("%lld\n", res);
}
void init() {
mp['A'] = 0, mp['G'] = 1, mp['C'] = 2, mp['T'] = 3;
}
signed main() {
init();
// freopen("in.txt", "r", stdin);
// int t; cin >> t; while(t--)
while(~scanf("%lld%lld", &n, &k))
solve();
return 0;
}
hdu2243-考研路茫茫――单词情结
思路
题目中要求所给单词至少出现一次,那么反向考虑就是所有的可能性减去不会出现的次数即可。那么就和上一题poj2778一样,稍微不同的就是不止是要算长度为L的不可能次数,也要计算小于L的所有不可能次数。
先考虑长度为L时的所有方案数,假设\(S_n\)表示长度为n的所有方案数,那么即可得
\(S_{n-1}=A+A^2+A^3+...+A^{n-1}\)
\(S_n=A+A^2+A^3+...+A^n=A(1+A+A^2+...+A^{n-1})=A*S_{n-1}+A\)
构造矩阵即是
\([\begin{matrix}
S_n\\
A
\end{matrix}]=
[\begin{matrix}
A & 1\\
0 & 1
\end{matrix}]
[\begin{matrix}
S_{n-1}\\
A
\end{matrix}]\)
在考虑如何求长度小于L的次数。如果\(A_n\)来表示长度为n的所有的不包含给定串的trie图,那么同理这个长度小于L的所有trie结果即是\(A,A^2,A^3,...,A^L\),就将所有矩阵的相应结果相加即可。再仔细看一下,那么可以发现这个式子其实是和上面那个构造矩阵的式子一模一样,只要将trie图所构造出来的矩阵的最后一列全部加上1即可,那么矩阵\([\begin{matrix}
A & 1\\
0 & 1
\end{matrix}]^n\)以后,\(m[0][1]\)位置所表示的即是\(A+A^2+...+A^{L-1}\)的结果,\(m[0][0]\)即是\(A^L\)的结果。那么全部加一下即可,具体也可以看一下代码中的内容。
代码
#include<iostream>
#include<map>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
typedef unsigned long long LL;
#define int LL
typedef pair<int, int> PII;
#define Martix Matrix
#define gcd __gcd
const int mod = 100000;
const int N = 30 + 10;
int n, k;
int tr[N][26], ne[N], idx;
bool vis[N];
char s[N];
struct Matrix {
int m[N][N];
Matrix() {
memset(m, 0, sizeof(m)); // 初始化矩阵
}
};
Matrix Multi(Matrix a, Matrix b) // 矩阵乘法
{
Matrix res;
for(int i = 0; i <= idx; i++) {
for(int j = 0; j <= idx; j++) {
for(int k = 0; k <= idx; k++) {
res.m[i][j] += a.m[i][k] * b.m[k][j];
}
}
}
return res;
}
Martix fastm(Martix a, int n) // 矩阵快速幂
{
Martix res;
for(int i = 0; i <= idx; i++) { // 等同于快速幂的res = 1的操作
res.m[i][i] = 1;
}
while(n) {
if(n & 1) res = Multi(res, a);
a = Multi(a, a);
n >>= 1;
}
return res;
}
void insert(char str[]) {
int p = 0;
for(int i = 0; str[i]; i++) {
int id = str[i] - 'a';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
vis[idx] = ne[idx] = 0;
}
p = tr[p][id];
}
vis[p] = true;
}
void build() {
queue<int> q;
for(int i = 0; i < 26; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
vis[0] = false, ne[0] = 0; idx = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s);
insert(s);
}
build();
Matrix x;
for(int i = 0; i <= idx; i++) {
if(vis[i]) continue;
for(int j = 0; j < 26; j++) {
if(!vis[tr[i][j]]) {
x.m[i][tr[i][j]]++;
}
}
}
idx++;
for(int i = 0; i <= idx; i++) {
x.m[i][idx] = 1;
}
x = fastm(x, k);
LL res = 0;
for(int i = 0; i <= idx; i++) {
res = res + x.m[0][i];
}
res -= 1;
Matrix a, b;
a.m[0][0] = 26;
a.m[0][1] = a.m[1][1] = 1;
a.m[1][0] = 0;
a = fastm(a, k);
LL res2 = a.m[0][0] + a.m[0][1] - 1;
printf("%llu\n", res2 - res);
}
signed main() {
// freopen("in.txt", "r", stdin);
// int t; cin >> t; while(t--)
while(~scanf("%lld%lld", &n, &k))
solve();
return 0;
}
hdu2825- Wireless Password
思路
AC自动机+状压DP
先建立AC自动机,同时对于所给的一些字符串结尾用二进制记录一下对应的状态。
\(dp[i][j][k]\):对于密码串在第i位,自动机上位置为j结点,状态为k的所有方案数。
转移方程:\(dp[i+1][p][k|vis[p]]+=dp[i][j][k],p=tr[j][t],0\leq t\leq 25\),这个t就是每个小写字母对应的ascii码。就是考虑当前第i位在自动机上对应的结点为j,状态为k,那么考虑下一位的情况,t即使枚举下一位所有可能的字符,对应自动机下一个跳到的结点就是\(p=tr[j][t]\),状态为\(k|vis[p]\)。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int mod = 20090717;
const int N = 100 + 10;
int n, m, k;
char s[N];
int tr[N][26], ne[N], idx;
int vis[N], dp[30][N][1100];
int mp[1100];
void insert(char s[], int id) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = s[i] - 'a';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = vis[idx] = 0;
}
p = tr[p][id];
}
vis[p] |= (1 << id);
}
void build() {
queue<int> q;
for(int i = 0; i < 26; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
ne[0] = vis[0] = 0;
idx = 0;
memset(dp, 0, sizeof dp);
for(int i = 0; i < m; i++) {
scanf("%s", s);
insert(s, i);
}
build();
dp[0][0][0] = 1;
for(int i = 0; i < n; i++) {
for(int j = 0; j <= idx; j++) {
for(int l = 0; l < (1 << m); l++) {
if(!dp[i][j][l]) continue;
for(int r = 0; r < 26; r++) {
int p = tr[j][r];
int t = l | vis[p];
dp[i + 1][p][t] += dp[i][j][l] % mod;
dp[i + 1][p][t] %= mod;
}
}
}
}
int res = 0;
for(int i = 0; i < (1 << m); i++) {
if(mp[i] >= k) {
for(int j = 0; j <= idx; j++) {
res += dp[n][j][i] % mod;
res %= mod;
}
}
}
printf("%d\n", res);
}
void init() {
for(int i = 0; i < (1 << 10); i++) {
mp[i] = 0;
for(int j = 0; j < 10; j++) {
if((i >> j) & 1) mp[i]++;
}
}
}
int main() {
init();
// freopen("in.txt", "r", stdin);
while(~scanf("%d%d%d", &n, &m, &k)) {
if(n == 0 && m == 0 && k == 0) break;
solve();
}
return 0;
}
hdu2296-Ring
思路
这题如果光只要输出最后的代价值为多少,那么就是一道简单的ac自动机dp问题,可是要考虑输出最优方案,最好的解决办法就是在dp的过程中同时记录每一位相对应的字符串即可。
\(dp[i][j]\):字符串在第i位,自动机上结点位j时的最大价值。
\(str[i][j]\):字符串在第i位,自动机上结点位j时的最大价值时,所对应的字符串。
转移方程:\(dp[i+1][p]=max(dp[i][j]+val[p]),p=tr[j][t]\),这里的p,t的意义和上面一题一样。在更新答案的过程中,实时更新这个str[i][j],最后遍历一遍结果得到最小字符串即可。具体看代码。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int mod = 20090717;
const int N = 1100 + 10;
char s[110][12];
int tr[N][26], ne[N], val[N], idx, dp[55][N];
char str[55][N];
char res[55], tmp[55], pre[55][N][55];
void insert(char s[], int d) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = s[i] - 'a';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = val[idx] = 0;
}
p = tr[p][id];
}
val[p] += d;
}
void build() {
queue<int> q;
for(int i = 0; i < 26; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 26; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
val[c] += val[ne[u]];
q.push(c);
}
}
}
}
bool cmp(char s1[], char s2[]) {
int len1 = strlen(s1);
int len2 = strlen(s2);
if(len1 != len2) return len1 < len2;
return strcmp(s1, s2) < 0;
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
idx = 0;
ne[0] = val[0] = 0;
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) {
scanf("%s", s[i]);
}
for(int i = 1; i <= m; i++) {
int x;
scanf("%d", &x);
insert(s[i], x);
}
build();
memset(dp, -1, sizeof dp);
memset(pre, 0, sizeof pre);
memset(res, 0, sizeof res);
dp[0][0] = 0;
int mx = 0;
for(int i = 0; i < n; i++) {
for(int j = 0; j <= idx; j++) {
if(dp[i][j] == -1) continue;
strcpy(tmp, pre[i][j]);
int len = strlen(tmp);
for(int k = 0; k < 26; k++) {
int p = tr[j][k];
tmp[len] = k + 'a';
tmp[len + 1] = 0;
// cout << tmp << endl;
if(dp[i][j] + val[p] > dp[i + 1][p]) {
dp[i + 1][p] = dp[i][j] + val[p];
strcpy(pre[i + 1][p], tmp);
}
else if(dp[i][j] + val[p] == dp[i + 1][p] && cmp(tmp, pre[i + 1][p])) {
strcpy(pre[i + 1][p], tmp);
}
}
}
}
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= idx; j++) {
if(mx < dp[i][j]) {
mx = dp[i][j];
strcpy(res, pre[i][j]);
}
else if(mx == dp[i][j] && cmp(pre[i][j], res)) {
strcpy(res, pre[i][j]);
}
}
}
if(mx == 0) puts("");
else printf("%s\n", res);
// cout << res << endl;
}
int main() {
// init();
// freopen("in.txt", "r", stdin);
int t; cin >> t; while(t--)
solve();
return 0;
}
hdu2457- DNA repair
思路
依然是那么个套路。
\(dp[i][j]\):到字符串第i位,自动机上第j位时的最小代价。
因为这个是考虑到修改的问题,按照常规先建立自动机,只有在dp的时候考虑一下这个结点是否合法,在合法的情况下考虑所给的字符串是否要修改,如果要修改那么代价就+1.
转移方程:\(dp[i+1][p]=min(dp[i][p],dp[i][j]+(s[i+1]==k)),p=tr[j][t]\),这里的p,t和hdu2825那题题解意义一样。k表示枚举的字符,即k为"AGCT"其中一个字符,判断是否和下一位相同。具体转移方程详见代码。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int mod = 100000;
const int N = 1000 + 10;
int n, kase;
int tr[N][4], idx, ne[N], dp[N][N];
bool vis[N];
char s[N];
map<char, int> mp;
void insert(char s[]) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = mp[s[i]];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
vis[idx] = 0;
ne[idx] = 0;
}
p = tr[p][id];
}
vis[p] = true;
}
void build() {
queue<int> q;
for(int i = 0; i < 4; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 4; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
idx = 0;
vis[0] = ne[0] = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s);
insert(s);
}
build();
scanf("%s", s + 1);
memset(dp, 0x3f, sizeof dp);
for(int i = 0; i <= idx; i++) dp[0][i] = 0;
int len = strlen(s + 1);
for(int i = 0; i < len; i++) {
for(int j = 0; j <= idx; j++) {
for(int k = 0; k < 4; k++) {
int val = mp[s[i + 1]] != k;
int p = tr[j][k];
if(!vis[p]) dp[i + 1][p] = min(dp[i + 1][p], dp[i][j] + val);
}
}
}
int res = 1e9;
for(int i = 0; i <= idx; i++) {
res = min(res, dp[len][i]);
}
printf("Case %d: %d\n", ++kase, res == 1e9 ? -1 : res);
}
signed main() {
mp['A'] = 0, mp['G'] = 1, mp['C'] = 2, mp['T'] = 3;
// freopen("in.txt", "r", stdin);
// int t; cin >> t; while(t--)
while(~scanf("%d", &n)) {
if(n == 0) break;
solve();
}
// solve();
return 0;
}
hdu3341-Lost's revenge
思路
和前几种问题稍稍有点不同,但大体思路框架还是一模一样的。
因为要考虑字符串的重排,那么就需要统计"AGCT"每个字符的个数,再拿dp开就变成了\(dp[i][j][][][][]\),那么空间一定爆炸,考虑到字符串的长度也是一定的,那么后面的四维可以压到一维,用字符串hash表示。相当于用字符串的进制表示。假如有cnt[0]个A,cnt[1]个G,cnt[2]个C,cnt[3]个T,那么考虑用进制表示每一个串,每出现一个A相当于在\((cnt[1]+1)*(cnt[2]+1)*(cnt[3]+1)\)上增加一位,同理G相当于\((cnt[2]+1)*(cnt[3]+1)\)上表示。
那么dp就可以考虑每次增加的是哪一个字母,同时记录一下最新的状态。
\(dp[i][j]\):在自动机上为i点时,状态为j的所有方案数。
对应的转移方程即为
\(dp[p][state]=dp[j][id]+cnt[p],p=tr[j][t],0\leq t<4,state=id+t*st[t]\),就是相当于枚举每一位增加的字母,然后id为当前状态,state为加上当前字母t之后的下一个状态,j为当前自动机上的结点,p为下一个结点。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int mod = 1000000007;
const int N = 500 + 10, M = 18;
int tr[N][4], idx, ne[N], dp[N][11 * 11 * 11 * 11], cnt[N];
char s[N];
map<char, int> mp;
int a[4], st[4], kase, n;
void insert(char s[]) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = mp[s[i]];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = cnt[idx] = 0;
}
p = tr[p][id];
}
cnt[p]++;
}
void build() {
queue<int> q;
for(int i = 0; i < 4; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 4; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
cnt[c] += cnt[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
ne[0] = cnt[0] = 0;
idx = 0;
memset(a, 0, sizeof a);
memset(st, 0, sizeof st);
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%s", s);
insert(s);
}
build();
memset(dp, -1, sizeof dp);
scanf("%s", s + 1);
int len = strlen(s + 1);
for(int i = 1; i <= len; i++) {
a[mp[s[i]]]++;
}
st[0] = (a[1] + 1) * (a[2] + 1) * (a[3] + 1);
st[1] = (a[2] + 1) * (a[3] + 1);
st[2] = a[3] + 1;
st[3] = 1;
dp[0][0] = 0;
int res = 0;
for(int A = 0; A <= a[0]; A++) {
for(int G = 0; G <= a[1]; G++) {
for(int C = 0; C <= a[2]; C++) {
for(int T = 0; T <= a[3]; T++) {
int id = A * st[0] + G * st[1] + C * st[2] + T * st[3];
for(int j = 0; j <= idx; j++) {
if(dp[j][id] == -1) continue;
for(int k = 0; k < 4; k++) {
if(A == a[0] && k == 0) continue;
if(G == a[1] && k == 1) continue;
if(C == a[2] && k == 2) continue;
if(T == a[3] && k == 3) continue;
int p = tr[j][k];
int state = id + st[k];
dp[p][state] = max(dp[p][state], dp[j][id] + cnt[p]);
}
}
}
}
}
}
int t = a[0] * st[0] + a[1] * st[1] + a[2] * st[2] + a[3] * st[3];
for(int i = 0; i <= idx; i++) {
res = max(res, dp[i][t]);
}
printf("Case %d: %d\n", ++kase, res);
}
int main() {
// freopen("in.txt", "r", stdin);
mp['A'] = 0, mp['G'] = 1, mp['C'] = 2, mp['T'] = 3;
// int t; cin >> t; while(t--)
// solve();
while(~scanf("%d", &n)) {
if(!n) break;
solve();
}
return 0;
}
hdu3247 - Resource Archiver
思路
n的数据范围只有10,一开始想的是设置状态\(dp[i][j]\)表示第一维当前选择好字符串的状态为i,那么第一维最对为(1<<10),此时AC自动机上的状态为j。转移方程即为\(dp[i|vis[p]][p]=min(dp[i][j]+1)\),\(vis[i]\)表示AC自动机上状态为i时所对应选择的字符串状态。但是这么考虑空间范围到了1e3*6e4,感觉会炸,无从下手学习了一波题解。
考虑到最后贡献转移出来的状态一定是可以被选择的字符串状态,有很多病毒串是无效的,就只要考虑那些能构成的字符串。有效状态只有10个串,可以预处理dis[i][j]表示第i个字符串和第j个字符串合法拼接的最短长度,通过ac自动机的一些性质可以很快求出来他们的最长后缀,也可以求出这个最短长度。最后\(dp[i][j]\)就变成了状态为i,AC自动机上合法状态为j的最短长度。转移方程具体看代码吧。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int inf = 0x3f3f3f3f;
const int mod = 20090717;
const int N = 6e4 + 10;
int tr[N][2], ne[N], st[N], idx, dp[1100][15], n, m, cnt, ok[15];
bool vis[N];
char s[N];
void insert(char s[], int d) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = s[i] - '0';
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = st[idx] = vis[idx] = 0;
}
p = tr[p][id];
}
if(d >= 0) st[p] |= (1 << d);
else vis[p] = true;
}
void build() {
queue<int> q;
for(int i = 0; i < 2; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 2; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
int d[N], dis[15][15];
void bfs(int s) {
memset(d, -1, sizeof d);
queue<int> q;
d[ok[s]] = 0;
q.push(ok[s]);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 2; i++) {
int v = tr[u][i];
if(d[v] < 0 && !vis[v]) {
d[v] = d[u] + 1;
q.push(v);
}
}
}
for(int i = 0; i < cnt; i++) {
dis[s][i] = d[ok[i]];
}
}
void init() {
memset(tr[0], 0, sizeof tr[0]);
ne[0] = vis[0] = st[0] = idx = 0;
cnt = 1;
memset(dis, 0x3f, sizeof dis);
memset(ok, 0, sizeof ok);
}
void solve() {
init();
for(int i = 0; i < n; i++) {
scanf("%s", s);
insert(s, i);
}
for(int i = 1; i <= m; i++) {
scanf("%s", s);
insert(s, -1);
}
build();
for(int i = 1; i <= idx; i++) {
if(st[i]) ok[cnt++] = i;
}
for(int i = 0; i < cnt; i++) {
bfs(i);
}
memset(dp, -1, sizeof dp);
dp[0][0] = 0;
for(int i = 0; i < (1 << n); i++) {
for(int j = 0; j < cnt; j++) {
if(dp[i][j] == -1) continue;
for(int k = 0; k < cnt; k++) {
if(dis[j][k] < 0) continue;
int nxt = i | st[ok[k]];
if(dp[nxt][k] == -1) {
dp[nxt][k] = dp[i][j] + dis[j][k];
}
else {
dp[nxt][k] = min(dp[nxt][k], dp[i][j] + dis[j][k]);
}
}
}
}
int res = 1e9;
for(int i = 0; i < cnt; i++) {
if(dp[(1 << n) - 1][i] != -1) res = min(res, dp[(1 << n) - 1][i]);
}
printf("%d\n", res);
}
int main() {
// init();
// freopen("in.txt", "r", stdin);
// int t; cin >> t; while(t--)
// solve();
while(~scanf("%d%d", &n, &m)) {
if(!n && !m) break;
solve();
}
return 0;
}
hdu4758- Walk Through Squares
思路
一个二维的自动机+状压dp,就相对于最基础的自动机改良了一下。
\(dp[i][j][k][st]\):走到位置(i, j)时,在自动机上k结点,状态为st的最终方案数。
因为有"R"和"D"的限制,就考虑要往下走还是往右走,枚举的时候考虑一下即可。
状态转移方程
\(dp[i+1][j][p][cnt[p]|t]+=dp[i][j][k][t]\),此时枚举的那个字母应该为"D"
\(dp[i][j+1][p][cnt[p]|t]+=dp[i][j][k][t]\),此时枚举的那个字母应该为"R"
具体结合代码看应该更容易理解
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int inf = 0x3f3f3f3f;
const int mod = 1000000007;
const int N = 200 + 10;
int tr[N][2], ne[N], cnt[N], idx, dp[110][110][N][4];
char s[N];
map<char, int> mp;
void insert(char s[], int t) {
int p = 0;
for(int i = 0; s[i]; i++) {
int id = mp[s[i]];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = cnt[idx] = 0;
}
p = tr[p][id];
}
cnt[p] |= (1 << t);
}
void build() {
queue<int> q;
for(int i = 0; i < 2; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 0; i < 2; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
cnt[c] |= cnt[ne[c]];
q.push(c);
}
}
}
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
ne[0] = cnt[0] = 0;
idx = 0;
int m, n;
scanf("%d%d", &m, &n);
for(int i = 0; i < 2; i++) {
scanf("%s", s);
insert(s, i);
}
build();
memset(dp, 0, sizeof dp);
dp[0][0][0][0] = 1;
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= m; j++) {
for(int k = 0; k <= idx; k++) {
for(int t = 0; t < 4; t++) {
if(dp[i][j][k][t] == 0) continue;
int p0 = tr[k][0], p1 = tr[k][1];
dp[i][j + 1][p0][cnt[p0] | t] = (1LL * dp[i][j][k][t] + 1LL * dp[i][j + 1][p0][cnt[p0] | t]) % mod;
// dp[i][j + 1][p0][cnt[p0] | t] %= mod;
dp[i + 1][j][p1][cnt[p1] | t] = (1LL * dp[i][j][k][t] + 1LL * dp[i + 1][j][p1][cnt[p1] | t]) % mod;
// dp[i + 1][j][p1][cnt[p1] | t] %= mod;
}
}
}
}
LL res = 0;
for(int i = 0; i <= idx; i++) {
res = (1LL * dp[n][m][i][3] + 1LL * res) % mod;
// res %= mod;
}
printf("%lld\n", res);
}
int main() {
mp['R'] = 0, mp['D'] = 1;
// freopen("in.txt", "r", stdin);
int t; cin >> t; while(t--)
solve();
return 0;
}
hdu4511-小明系列故事――女友的考验
思路
表面上是个图论题,其实是个ac自动机。如果在比赛中碰到我应该也想不到用ac自动机来搞。
通过建立AC自动机形成的trie图来表示哪些方式是不能到达终点的,然后建立dp。
\(dp[i][j]\):走到结点i,在自动机上结点为j的最小距离
\(dp[k][p]=min(dp[i][j]+dis(i,k)),p=tr[j][k],i<k\leq n\)表示从i走到k结点的最小代价,注意一些限制条件即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define gcd __gcd
const int inf = 0x3f3f3f3f;
const int mod = 1000000007;
const int N = 500 + 10;
int tr[N][55], ne[N], idx;
bool vis[N];
double x[N], y[N], dp[55][N];
int n, m;
vector<int> s;
void insert() {
int len = s.size(), p = 0;
for(int i = 0; i < len; i++) {
int id = s[i];
if(!tr[p][id]) {
tr[p][id] = ++idx;
memset(tr[idx], 0, sizeof tr[idx]);
ne[idx] = vis[idx] = 0;
}
p = tr[p][id];
}
vis[p] = true;
}
void build() {
queue<int> q;
for(int i = 1; i <= n; i++) {
if(tr[0][i]) q.push(tr[0][i]);
}
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = 1; i <= n; i++) {
int c = tr[u][i];
if(!c) tr[u][i] = tr[ne[u]][i];
else {
ne[c] = tr[ne[u]][i];
vis[c] |= vis[ne[c]];
q.push(c);
}
}
}
}
double dis(int a, int b) {
return sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
}
void solve() {
memset(tr[0], 0, sizeof tr[0]);
idx = 0;
ne[0] = vis[0] = 0;
for(int i = 1; i <= n; i++) {
scanf("%lf%lf", &x[i], &y[i]);
}
for(int i = 1; i <= m; i++) {
int k;
scanf("%d", &k);
s.clear();
for(int i = 1; i <= k; i++) {
int x; scanf("%d", &x);
s.push_back(x);
}
insert();
}
build();
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= idx; j++) {
dp[i][j] = 1e18;
}
}
dp[1][tr[0][1]] = 0;
for(int i = 1; i < n; i++) {
for(int j = 0; j <= idx; j++) {
if(dp[i][j] == inf) continue;
for(int k = i + 1; k <= n; k++) {
int p = tr[j][k];
if(!vis[p]) dp[k][p] = min(dp[k][p], dp[i][j] + dis(i, k));
}
}
}
double res = 1e18;
for(int i = 0; i <= idx; i++) {
res = min(res, dp[n][i]);
}
if(res == 1e18) puts("Can not be reached!");
else printf("%.2f\n", res);
}
int main() {
// freopen("in.txt", "r", stdin);
// int t; cin >> t; while(t--)
// solve();
while(~scanf("%d%d", &n, &m)) {
if(!n && !m) break;
solve();
}
return 0;
}