您的位置:首页 > 其它

动态规划和字符串匹配(KMP、AC自动机)

2013-04-17 19:12 351 查看
最近学了关于字符串匹配主要的两种方法,做了一些题目,发现这可以动态规划结合,题目中往往有“……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;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: