最近学了关于字符串匹配主要的两种方法,做了一些题目,发现这可以动态规划结合,题目中往往有“……T是S的子串……”这类字眼,而且答案要求“最少、最多、多少种”一类的问题。
解决这类问题往往是要记录一个“匹配到哪一位”的状态,然后考虑当前状态可以更新哪些新的状态,下面列举两道题目:
KMP
有两个字符串S和T(1 ≤ |S| ≤10000, 1 ≤ |T| ≤ 1000),问题是至少要从字符串S中删掉多少个字符,才能使得字符串T不是字符串S的子串。
很容易想到一个这样的动态规划:以f(i,j)记录S字符串第i位,匹配了T的前j位,最少需要删除多少位。然后对于每一位,有删除和不删除两种情况。下面是一个例子:
|
|
|
i |
i + 1 |
|
S |
…… |
A |
B |
A |
…… |
T |
|
A |
B |
C |
|
|
|
|
j |
|
|
我们用当前的状态去更新新的状态,现在需要考虑第i+1位的A是否删除,如果删除,匹配的个数j不变,那么:f(i+1,j)= f(i,j)+ 1;如果不删除,匹配的个数是多少呢?
|
|
|
i |
i + 1 |
|
|
S |
…… |
A |
B |
A |
…… |
…… |
T |
|
A |
B |
C |
|
|
T’ |
|
|
|
A |
B |
C |
|
|
|
j |
|
|
|
当然,不能够一个个枚举。这时可以发现,求新的匹配的个数正好是KMP中匹配失败后的所需要求的。
那么,这样时间复杂度貌似就是预处理的O(|T|)加上状态O(|S| × |T|)。但后来发现求新的匹配个数时超时,因为很多求next的过程是一样的。如:
|
1 |
2 |
3 |
i = 4 |
5 |
6 |
|
|
S |
A |
B |
A |
B |
C |
A |
|
|
T |
A |
B |
A |
B |
A |
|
|
|
T’ |
|
|
A |
B |
A |
B |
A |
|
T’’ |
|
|
|
|
A |
B |
A |
…… |
首先,i = 4, j = 4,也就是T串,现在匹配了4个,现在考虑不删除i+1位,则要求出匹配的个数,得到了T’,然而T’就是f(i,next[j]),发现还是不行,则又有T’’ : f(i,next[ next[j ] ]),还是不行,那么就是一个也匹配不了。那么在求完了f(i,j)后,结果也是T’、T’’的结果,所以可以同时计算这几个的结果。这样就不超了。
AC自动机
给出n(1 ≤ n ≤ 20, 长度小于 20)个字符串,要求构造长度为K的字符串,使得这n个字符串出现的次数最多。
样例输入:
3 7
ABA
CB
ABACB
样例输出:
4
样例解释:
构造出 ABACBCB,则有1个ABA,2个CB,1个ABACB。
设计一个这样的动态规划:f(i, j)记录下,已经了构造第i个字符,匹配的状态为字母树的标号为j的结点(这个可以用静态数组来保存字母树,然后数组第j个结点就是标号为j的结点),最多可以出现多少个字符串,然后可以枚举第 i+1 构造的字母,去更新f(i+1, ?)。这个可以在字母树上沿着失败指针走一次即可。
#include <cstdio>
#include <cstring>
using namespace std;
const int inf = 0x3f3f3f3f;
char S[10007], T[1007];
int next[1007];
int f[10007][1007];
int main() {
freopen("necklace.in", "r", stdin);
freopen("necklace.out", "w", stdout);
scanf("%s\n%s", S, T);
int n = strlen(S);
int m = strlen(T);
int j = -1;
next[0] = -1;
for (int i = 1; i < m; i ++) {
while (j != -1 && T[j + 1] != T[i])
j = next[j];
if (T[j + 1] == T[i])
j ++;
next[i] = j;
}
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
f[i][j] = inf; //全部为无穷大
if (S[0] == T[0]) { //f[i] 初始化
f[0][1] = 0;
f[0][0] = 1;
}
else
f[0][0] = 0;
for (int i = 0; i < n - 1; i ++) {
for (int j = -1; j < m - 1; j ++) //-1表示一个也匹配不了,0为匹配一个,以此类推 使用时下表偏移+1
f[i + 1][j + 1] <?= f[i][j + 1] + 1; //删除的情况
for (int j = m - 2; j >= -1; j --)
if (f[i][j + 1] != inf) { //有可能达到该状态
int tmp = j, tmp2 = inf; //tmp为当前状态, tmp2为最小可以用来更新的值
while (tmp != -1 && T[tmp + 1] != S[i + 1]) {
tmp2 <?= f[i][tmp + 1];
f[i][tmp + 1] = inf; //该状态同f(i, j),所以下次不用计算了。
tmp = next[tmp];
}
tmp2 <?= f[i][tmp + 1];
f[i][tmp + 1] = inf;
if (T[tmp + 1] == S[i + 1]) tmp ++;
f[i + 1][tmp + 1] <?= tmp2;
}
}
int ans = inf;
for (int j = 0; j < m; j ++) //结果不能匹配m个,
ans <?= f[n - 1][j];
printf("%d\n", ans);
return 0;
}
#include <cstdio>
#include <cstring>
using namespace std;
struct Trie {
Trie *son[3], *next;
int cnt;
Trie() {
for (int i = 0; i < 3; i ++) son[i] = NULL;
cnt = 0;
}
}mem[307], *root = mem, *que[307]; //静态数组, 下标为0的是根
int tot, f[1007][307];
char dat[17];
Trie* New() { //创建新结点
tot ++;
return mem + tot;
}
int main() {
freopen("combos.in", "r", stdin);
freopen("combos.out", "w", stdout);
int n, k;
scanf("%d%d\n", &n, &k);
for (int i = 0; i < n; i ++) { //建立字母树
scanf("%s\n", dat);
int len = strlen(dat);
Trie *now = root;
for (int j = 0; j < len; j ++) {
int p = dat[j] - 'A';
if (! now -> son[p]) now -> son[p] = New();
now = now -> son[p];
}
now -> cnt ++;
}
int head = 0, tail = 0;
que[0] = root;
root -> next = NULL; //建立AC自动机
while (head <= tail) {
Trie *now = que[head];
head ++;
for (int i = 0; i < 3; i ++)
if (now -> son[i]) {
tail ++;
que[tail] = now -> son[i];
now -> son[i] -> next = root;
if (now != root) {
Trie *tmp = now -> next;
while (tmp) {
if (tmp -> son[i]) {
now -> son[i] -> next = tmp -> son[i];
break;
}
tmp = tmp -> next;
}
}
}
}
memset(f, -1, sizeof(f)); //初始化
f[0][0] = 0;
for (int i = 0; i < k; i ++) //构造第i位
for (int j = 0; j <= tot; j ++) //点的编号
if (f[i][j] > -1) { //该状态可以实现
for (int p = 0; p < 3; p ++) { //枚举第i+1个字母
Trie *now = mem + j; //编号为j的点
while (! now -> son[p] && now -> next) now = now -> next;
if (! now -> son[p]) {
f[i+1][0] >?= f[i][j]; //如果没找到,则更新根结点
continue;
}
now = now -> son[p];
Trie *tmp = now;
int cntf = 0; //计数:可以新增多少个
while (tmp) {
cntf += tmp -> cnt;
tmp = tmp -> next;
}
f[i+1][now-mem] >?= f[i][j] + cntf;
}
}
int ans = 0;
for (int i = 0; i <= tot; i ++) ans >?= f[k][i];
printf("%d\n", ans);
return 0;
}