题目地址:
https://www.acwing.com/problem/content/1055/
生物学家终于发明了修复DNA的技术,能够将包含各种遗传疾病的DNA片段进行修复。为了简单起见,DNA看作是一个由’A’, ‘G’ , ‘C’ , ‘T’构成的字符串。修复技术就是通过改变字符串中的一些字符,从而消除字符串中包含的致病片段。例如,我们可以通过改变两个字符,将DNA片段”AAGCAG”变为”AGGCAC”,从而使得DNA片段中不再包含致病片段”AAG”,”AGC”,”CAG”,以达到修复该DNA片段的目的。需注意,被修复的DNA片段中,仍然只能包含字符’A’, ‘G’, ‘C’, ‘T’
。请你帮助生物学家修复给定的DNA片段,并且修复过程中改变的字符数量要尽可能的少。
输入格式:
输入包含多组测试数据。
每组数据第一行包含整数
N
N
N,表示致病DNA片段的数量。
接下来
N
N
N行,每行包含一个长度不超过
20
20
20的非空字符串,字符串中仅包含字符’A’, ‘G’ , ‘C’ , ‘T’,用以表示致病DNA片段。
再一行,包含一个长度不超过
1000
1000
1000的非空字符串
s
s
s,字符串中仅包含字符’A’, ‘G’ , ‘C’ , ‘T’,用以表示待修复DNA片段。
最后一组测试数据后面跟一行,包含一个
0
0
0,表示输入结束。
输出格式:
每组数据输出一个结果,每个结果占一行。
输入形如Case x: y
,其中
x
x
x为测试数据编号(从
1
1
1开始),
y
y
y为修复过程中所需改变的字符数量的最小值,如果无法修复给定DNA片段,则
y
y
y为
−
1
-1
−1。
数据范围:
1
≤
N
≤
50
1≤N≤50
1≤N≤50
设字母表只有 A C G T ACGT ACGT四个字母,其实就是给定若干模式串,再给定一个主串,问主串修改多少个位置,可以使得主串不含任一模式串作为子串。
先将所有模式串做成一个AC自动机,设 f [ i ] [ u ] f[i][u] f[i][u]是在自动机上走了 i i i步停留在 u u u节点的时候,最少要修改多少个字符能使得主串不含任一模式串。在自动机上走的过程,其实就是扫描主串的过程,把在自动机上走一个不同于当前扫描字符的边视为一单位的代价,那么问题其实就是问,要求路上走不到任何匹配点的情况下,在自动机上走 l s l_s ls步的最小代价是多少,视为 f [ i ] [ u ] f[i][u] f[i][u]的”新定义“。那么我们可以枚举 u u u的四条出边(对应四个不同字母),首先找到不产生匹配的出边,假设 u → p u\to p u→p并且 p p p不产生匹配,设 s s s下标从 1 1 1开始,那么有 f [ i + 1 ] [ p ] = min u → p { f [ i ] [ u ] + 1 s i + 1 ≠ u → p } f[i+1][p]=\min_{u\to p}\{f[i][u]+1_{s_{i+1}\ne u\to p}\} f[i+1][p]=u→pmin{f[i][u]+1si+1=u→p}代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m;
int tr[N][4], idx;
bool word[N];
int q[N], ne[N];
char s[N];
int mp['T' + 1];
int f[N][N];
void insert() {
int p = 0;
for (int i = 0; s[i]; i++) {
int c = mp[s[i]];
if (!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
// 标记一下p点为匹配点,匹配点还有p的ne指针反向上的所有点,也要标记,见下文
word[p] = true;
}
void build() {
int hh = 0, tt = 0;
for (int i = 0; i < 4; i++)
if (tr[0][i]) q[tt++] = tr[0][i];
while (hh < tt) {
int t = q[hh++];
for (int i = 0; i < 4; i++) {
int p = tr[t][i];
// 需要做word[p] |= word[ne[p]]这一步,如果ne[p]是匹配点,p点也是匹配点,
if (p) ne[p] = tr[ne[t]][i], q[tt++] = p, word[p] |= word[ne[p]];
else tr[t][i] = tr[ne[t]][i];
}
}
}
int main() {
// 四个字母对应的边的下标
mp['A'] = 0, mp['G'] = 1, mp['C'] = 2, mp['T'] = 3;
int T = 1;
while (scanf("%d", &n), n) {
memset(tr, 0, sizeof tr);
memset(word, 0, sizeof word);
memset(ne, 0, sizeof ne);
idx = 0;
for (int i = 0; i < n; i++) {
scanf("%s", s);
insert();
}
build();
scanf("%s", s + 1);
m = strlen(s + 1);
memset(f, 0x3f, sizeof f);
f[0][0] = 0;
// 遍历主串,即遍历步数
for (int i = 0; i < m; i++)
// 遍历所有节点,j是在自动机上走了i步走到的点
for (int j = 0; j <= idx; j++) {
// 如果j节点是匹配点,则不能走,直接略过
// 如果j走不到,也直接略过
if (word[j] || f[i][j] == 0x3f3f3f3f) continue;
// 枚举4个方向
for (int k = 0; k < 4; k++) {
int p = tr[j][k];
// p是匹配点,不能走
if (word[p]) continue;
// 更新i + 1步走到p的最小代价
f[i + 1][p] = min(f[i + 1][p], f[i][j] + (mp[s[i + 1]] != k));
}
}
int res = 0x3f3f3f3f;
for (int i = 0; i <= idx; i++) res = min(res, f[m][i]);
if (res == 0x3f3f3f3f) res = -1;
printf("Case %d: %d\n", T++, res);
}
}
时间复杂度 O ( l s N ) O(l_sN) O(lsN),空间 O ( N ) O(N) O(N)。