最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
题目描述
本题 有限字符集 为 {A,G,C,T}
题目给定 n 个 模式串 Ti(1≤i≤n) 和 一个 主串 S
每次可以修改 主串 中的一个字符 S[i]
要求进行最少次 修改操作,使得 主串S 中不存在 模式串 Ti(1≤i≤n) 的子串
求出该 操作次数 ,如果不行,输出 −1
分析
阅读本题解的前置知识: 自动机,AC自动机, 线性DP
基本思路和上一题 AcWing 1052. 设计密码【线性DP+KMP自动机模型】 一致
我就照搬一下上一题题解中的分析思路了
采用 线性扫描 的方法,进行 动态规划
考虑一个 单主串 与 多模式串 的 匹配算法,考虑使用 AC自动机 进行分析
考虑 阶段i 需要记录的状态也和上一题类似,是 自动机 的 状态j
自动机模型分析
依然举样例来分析,因为不同模式串构成的字典树不同,没法一概而论
以 状态j=2为例
根据 AC自动机 的状态转移:
transj,c={j+1,ifsi=ch transfailj,chelse
对应于本样例就是:
- ch是b,则最大长度增加一,对应到 fi+1,j+1
- ch不是b,则根据自动机跳到状态 transfailj,ch,对应到 fi+1,transfailj,ch
闫氏DP分析法
状态表示—集合fi,j: 考虑主串中前 i 个字符,且当前自动机状态是 j 的方案
状态表示—属性fi,j: 方案的操作数最小 Min
状态计算fi,j:
fi+1,transj,c=min
【注】:本题是求最小值,因此要把非初始状态初始化为正无穷
初始状态: f_{0,0}
目标状态: f_{n,j} \quad j枚举的是所有自动机的状态
Code 朴素写法
时间复杂度:O(4Len_S \sum Len_{T_i})
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55, M = 1010, K = 25;
const int INF = 0x3f3f3f3f;
int n;
char s[M];
int tr[N * K][4], cnt[N * K], idx;
int fail[N * K], q[N * K];
int f[M][N * K];
int get(char c)
{
if (c == 'A') return 0;
if (c == 'G') return 1;
if (c == 'C') return 2;
return 3;
}
void insert(char s[])
{
int p = 0;
for (int i = 0; s[i]; ++ i)
{
int u = get(s[i]);
if (!tr[p][u]) tr[p][u] = ++ idx;
p = tr[p][u];
}
cnt[p] = 1;
}
void build()
{
int tt = -1, hh = 0;
for (int i = 0; i < 4; ++ i)
if (tr[0][i])
q[ ++ tt] = tr[0][i];
while (hh <= tt)
{
int u = q[hh ++ ];
for (int i = 0; i < 4; ++ i)
{
if (tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i],
q[ ++ tt] = tr[u][i];
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void solve()
{
//initializing
memset(fail, 0, sizeof fail);
memset(cnt, 0, sizeof cnt);
memset(f, 0x3f, sizeof f);
memset(tr, 0, sizeof tr);
f[0][0] = 0;
idx = 0;
//input and build the AC Automaton
for (int i = 1; i <= n; ++ i) cin >> s, insert(s);
cin >> s + 1; build();
int n = strlen(s + 1);
//dp
for (int i = 0; i < n; ++ i)
{
for (int j = 0; j <= idx; ++ j)
{
//枚举下一个字符
for (int k = 0; k < 4; ++ k)
{
int cost = get(s[i + 1]) != k; //修复下一个字符的费用
//递归找到下一层的状态
int p = tr[j][k];
bool flag = true;
int temp = p;
while (temp)
{
if (cnt[temp])
{
flag = false;
break;
}
temp = fail[temp];
}
//找到了,就更新
if (flag) f[i + 1][p] = min(f[i + 1][p], f[i][j] + cost);
}
}
}
int res = INF;
for (int j = 0; j <= idx; ++ j) res = min(res, f[n][j]);
if (res == INF) res = -1;
cout << res << endl;
}
int main()
{
int T = 1;
while (cin >> n, n)
{
cout << "Case " << T ++ << ": ";
solve();
}
return 0;
}
Code(y总的优化思路)
OI爷的想法就是牛!
考虑一个长的匹配串,如果他的某一个前缀子串的最大相等前后缀(就是fail[j]) 出现了另一个模式串匹配成功的状态,则我们认为该状态就是 匹配成功状态
这样我们就不用每次枚举第 i+1 个字符后,再额外进行 fail 跳转枚举了
只需在建 AC自动机 时,构建 fail 指针的跳转时,顺便记录 跳转到的状态 是否也会匹配上即可
由于 BFS 保证枚举状态 j 时,所有 长度小于当前状态 的模式串以及 fail 指针跳转都构建好了
因此,可以保证建立 fail 指针时,他 跳转的状态 以及他 跳转的跳转的状态 是否是 匹配成功的状态 已经被更新出了
于是,通过该思路,就可以进行 y总的代码优化了
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55, M = 1010, K = 25;
const int INF = 0x3f3f3f3f;
int n;
char s[M];
int tr[N * K][4], cnt[N * K], idx;
int fail[N * K], q[N * K];
int f[M][N * K];
int get(char c)
{
if (c == 'A') return 0;
if (c == 'G') return 1;
if (c == 'C') return 2;
return 3;
}
void insert(char s[])
{
int p = 0;
for (int i = 0; s[i]; ++ i)
{
int u = get(s[i]);
if (!tr[p][u]) tr[p][u] = ++ idx;
p = tr[p][u];
}
cnt[p] = 1;
}
void build()
{
int tt = -1, hh = 0;
for (int i = 0; i < 4; ++ i)
if (tr[0][i])
q[ ++ tt] = tr[0][i];
while (hh <= tt)
{
int u = q[hh ++ ];
for (int i = 0; i < 4; ++ i)
{
if (tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i],
cnt[tr[u][i]] |= cnt[fail[tr[u][i]]],//看他跳转的状态是否是匹配成功状态
q[ ++ tt] = tr[u][i];
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void solve()
{
//initializing
memset(fail, 0, sizeof fail);
memset(cnt, 0, sizeof cnt);
memset(f, 0x3f, sizeof f);
memset(tr, 0, sizeof tr);
f[0][0] = 0;
idx = 0;
//input and build the AC Automaton
for (int i = 1; i <= n; ++ i) cin >> s, insert(s);
cin >> s + 1; build();
int n = strlen(s + 1);
//dp
for (int i = 0; i < n; ++ i)
{
for (int j = 0; j <= idx; ++ j)
{
//枚举下一个字符
for (int k = 0; k < 4; ++ k)
{
int cost = get(s[i + 1]) != k; //修复下一个字符的费用
int p = tr[j][k]; //下一个字符的自动机状态
if (!cnt[p]) f[i + 1][p] = min(f[i + 1][p], f[i][j] + cost);
}
}
}
int res = INF;
for (int j = 0; j <= idx; ++ j) res = min(res, f[n][j]);
if (res == INF) res = -1;
cout << res << endl;
}
int main()
{
int T = 1;
while (cin >> n, n)
{
cout << "Case " << T ++ << ": ";
solve();
}
return 0;
}
## 太难了,所以没有评论
彩铅同志咋又双叒叕来了?
%%%