|
|
#include <bits/stdc++.h>
|
|
|
using namespace std;
|
|
|
const int INF = 0x3f3f3f3f;
|
|
|
const int N = 1010; // 致病DNA片段的数量上限50,每个DNA片段的长度不超过20,所以Trie树的结点个数最多不超过50*20=1000个节点
|
|
|
|
|
|
int n, m;
|
|
|
int tr[N][4], cnt[N], idx; // Trie树,每个节点最多四条出边ATGC,映射就0123,cnt表示每个节点为结尾的结束标识,idx:配合创建Trie树的导航员
|
|
|
int q[N], ne[N]; // AC自动机需要的队列数组,失配指针数组,ne[u]描述在u结点失配时,找到root~u的最长后缀相同的最长前缀root~v
|
|
|
char str[N]; // 第一轮当作模板串的录入存储器,完成构建Trie树后,第二轮当做文本串的存储器
|
|
|
int f[N][N]; // f[i][j]:DNA扫描到第i个字母,且当前走到了trie里面的第j个节点 的 所有操作方案中,最少的修改数量
|
|
|
|
|
|
int get(char c) {
|
|
|
if (c == 'A') return 0;
|
|
|
if (c == 'T') return 1;
|
|
|
if (c == 'G') return 2;
|
|
|
return 3;
|
|
|
}
|
|
|
|
|
|
void insert() { // 利用模板串构建Trie树
|
|
|
int p = 0;
|
|
|
for (int i = 0; str[i]; i++) {
|
|
|
int t = get(str[i]);
|
|
|
if (tr[p][t] == 0) tr[p][t] = ++idx;
|
|
|
p = tr[p][t];
|
|
|
}
|
|
|
cnt[p] = 1; // 记录结束标识
|
|
|
}
|
|
|
|
|
|
// 构建AC自动机
|
|
|
void bfs() {
|
|
|
int hh = 0, tt = -1;
|
|
|
for (int i = 0; i < 4; i++) // 利用bfs进行构建,把第一层的节点入队列
|
|
|
if (tr[0][i]) q[++tt] = tr[0][i];
|
|
|
|
|
|
while (hh <= tt) {
|
|
|
int p = q[hh++];
|
|
|
for (int i = 0; i < 4; i++) { // 尝试四个出边
|
|
|
int &c = tr[p][i];
|
|
|
if (!c)
|
|
|
c = tr[ne[p]][i];
|
|
|
else {
|
|
|
ne[c] = tr[ne[p]][i];
|
|
|
q[++tt] = c;
|
|
|
// c是不是终止节点,有两个判断来源:
|
|
|
// 1、自己本身就是模示串A的终点
|
|
|
// 2、它的失配指针指向的结点是模示串B的终点
|
|
|
cnt[c] |= cnt[ne[c]];
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
int T = 1;
|
|
|
while (scanf("%d", &n), n) {
|
|
|
memset(tr, 0, sizeof tr);
|
|
|
memset(cnt, 0, sizeof cnt);
|
|
|
memset(ne, 0, sizeof ne);
|
|
|
idx = 0;
|
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
scanf("%s", str);
|
|
|
insert();
|
|
|
}
|
|
|
|
|
|
// 构建AC自动机
|
|
|
bfs();
|
|
|
|
|
|
// str:长度不超过1000的非空字符串,字符串中仅包含字符A, G , C , T,用以表示待修复DNA片段
|
|
|
// Q:为什么不直接从索引0开始读入呢?
|
|
|
// 答:你可以尝试以索引0开始读入,你会发现,后面的状态转移方程就无法使用f[i-1][j]
|
|
|
// 因为数组下标越界了~哈哈,看来啊,都是先有状态转移方程,后有初始值,枚举序等东西
|
|
|
scanf("%s", str + 1);
|
|
|
m = strlen(str + 1); // 长串的长度
|
|
|
|
|
|
memset(f, 0x3f, sizeof f); // 初始化动态规划数组,预求最小,先设最大
|
|
|
f[0][0] = 0; // 匹配了前0个字符,在Trie树中的节点号是0的情况下,此时代价为0
|
|
|
|
|
|
// 开始DP填表,一维是长串走到哪个字符,二维是AC自动机中走到哪个节点
|
|
|
for (int i = 0; i < m; i++) // 枚举长串字符
|
|
|
for (int j = 0; j <= idx; j++) // 枚举节点
|
|
|
for (int k = 0; k < 4; k++) { // AGCT四种字符
|
|
|
int t = get(str[i + 1]) != k; // 下一个字符str[i+1],如果与目标字符一致,代价为0,否则代价为1
|
|
|
int p = tr[j][k]; // fail失配指针指向位置
|
|
|
// 如果p节点不是病毒DNA的终止节点,那么它的最小代价可以由f[i][j]+t转移
|
|
|
if (!cnt[p]) f[i + 1][p] = min(f[i + 1][p], f[i][j] + t);
|
|
|
}
|
|
|
|
|
|
// 找出最小值
|
|
|
int res = INF;
|
|
|
for (int i = 0; i <= idx; i++) res = min(res, f[m][i]);
|
|
|
|
|
|
// 没有最小值,返回-1
|
|
|
if (res == INF) res = -1;
|
|
|
printf("Case %d: %d\n", T++, res);
|
|
|
}
|
|
|
return 0;
|
|
|
} |