AcWing 1053 修复DNA

题目传送门

一、前导知识

\(AcWing\) \(1282\). 搜索关键词

二、AC自动机分析

本题是多模式串,单长串的问题,所以将不允许出现的短串建\(AC\)自动机,在每个串末尾打一个标记。

三、动态规划分析

设置\(f[i][j]\)表示处理完文本串的前\(i\)个字符,匹配到\(AC\)自动机中的\(j\)号节点的方案中修改字符数量的最小值为多少。

来研究一下从当前状态,可以转化到哪些状态,同时携带过去什么样的信息。
按上面的描述方法,那么目前肯定是没有存在\(DNA\)病毒片断的。那么,目前的状态,可以通过引出的四条边向其它状态转移,当然,也可能是通过失配指针进行转移。
如果要转移去的状态,存在片断结束标识,那么是不能走的,因为走了就意味着包含了病毒片断。
不存在结束标识的结点,是可以走的,这里面还划分两种情况:

  • 走的这条路与长串的当前字符一致。
    啥也不用修改,代价也没有增加。

  • 走的这条路与长串的当前字符不一致。
    需要修改当前的字符,代价增加\(1\)

四、经验总结

  • 不包含的处理方法
    cnt[p] |= cnt[fail[p]];
    是因为本题要求的是不能包含模板串,
    如果当前结点的失配指针结点有标识,那么当前结点也需要进行标识,表示:
    abc不可以,那么xxabcxx也是不行的,否则就是存在病毒片断了~
    说的再白一点,就是标准的AC自动机只解决完整匹配的问题,这里要解决的是“不包含”,那么就需要增加这句话了!

这里只需要标记后缀就可以了,比如存在 xxabcxx这样的路径,由于在处理到xxabc时,就标识了xxabccnt值为
abccnt值,此结点的后续部分就不会被走到,因为它是一个树的形式,所以只需要标识后缀就可以了。

  • AC自动机DP的套路
  1. 线性枚举每个长串的字符
  2. 枚举AC自动机的每个状态结点
  3. 枚举每个状态结点的出边
  4. 根据题意决策可否转移,转移的话成本是多高。

五、实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 1010; //50个DNA片断,每个片断的长度不超过20,那么所有的结点个数就是不超过1000
const int INF = 0x3f3f3f3f;
int n;
//Trie树
int tr[N][4], cnt[N], idx; //’A’,‘G’,‘C’,‘T’
//AC自动机
int q[N], fail[N];
string str;

//DP数组
/*类比前一道题,AcWing 1052. 设计密码
f[i][j]:密码已生成了i位,并且第i位匹配在子串中的位置为j时的方案数。
本题f[i][j]:前i个字母,并且当前走到了trie里面的第j个位置,所有操作方案中,最少修改的数量

AC自动机的题目,一般都可以从KMP的题目类比过来。
*/
int f[N][N];

//转换函数
int get(char c) {
    if (c == 'A') return 0;
    if (c == 'T') return 1;
    if (c == 'G') return 2;
    return 3;
}

//构建Trie树
void insert() {
    int p = 0;
    for (int i = 0; i < str.size(); i++) {
        int t = get(str[i]);
        if (!tr[p][t]) tr[p][t] = ++idx;
        p = tr[p][t];
    }
    cnt[p] = 1;//标识有结尾单词
}

//构建AC自动机
void build() {
    int hh = 0, tt = -1;
    //一级结点入队列
    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++) {//遍历所有可能的子节点:A-0,G-1,C-2,T-3
            int p = tr[t][i];
            if (!p) tr[t][i] = tr[fail[t]][i];
            else {
                fail[p] = tr[fail[t]][i];
                q[++tt] = p;
                /*
                下面这句不是AC自动机标准模板的内容,是因为本题要求的是不能包含模板串,
                 如果当前结点的失配指针结点有标识,那么当前结点也需要进行标识,表示:
                 abc不可以,那么xxxxxabc也是不行的,否则就是存在病毒片断了~
                 说的再白一点,就是标准的AC自动机只解决完整匹配的问题,这里要解决的是“不包含”,那么就需要增加这句话了!

                 这里我们只需要标记后缀就可以了,比如存在 xxabcxx这样的路径,由于我们在处理到xxabc时,就标识了xxabc的cnt值为
                 abc的cnt值,此结点的后续部分就不会被走到,因为它是一个树的形式,所以我们只需要标识后缀就可以了。
                */
                cnt[p] |= cnt[fail[p]];//这句话太重要了~!
            }
        }
    }
}

int main() {
    int T = 1;
    while (cin >> n, n) {
        //多组数据,每次清空
        memset(tr, 0, sizeof tr);
        memset(cnt, 0, sizeof cnt);
        memset(fail, 0, sizeof fail);
        memset(f, 0x3f, sizeof f);
        idx = 0;

        //构建Trie树
        for (int i = 1; i <= n; i++) cin >> str, insert();

        //构建AC自动机
        build();

        //读入:待修复DNA片段
        cin >> str;

        //开始DP
        f[0][0] = 0; //一个字母都没有,状态显然是0
        /*现在做完了第i个字母了,然后当前已经走到AC自动机的第j个位置,并且前面不包含任何一个病毒串的情况下
                        我们来枚举下一个位置该填充哪个字母,我们准备填充字母k*/
        //下面三层循环的枚举步骤需要记忆一下,一般都是这样的三层循环,注意顺序不能反~
        for (int i = 0; i < str.size(); i++)            //枚举str长度
            for (int j = 0; j <= idx; j++)              //枚举AC自动机的结点
                for (int k = 0; k < 4; k++) {           //枚举当前位置选哪个字母,即边
                    int p = tr[j][k];                   //可能到达的结点
                    //判断一下p这个位置是不是安全节点,如果大于0,表示存在以此结点为终点的致病片断,不能走这条路径
                    if (!cnt[p]) {
                        //当前这条路线,当前这步,是否与长串的当前位置一致,一致则没有代价,否则代价是1
                        int cost = get(str[i]) == k ? 0 : 1;
                        //当前状态,可以通过+cost的代价,到达下一个状态
                        f[i + 1][p] = min(f[i + 1][p], f[i][j] + cost);
                    }
                }

        //预求最小,先设最大
        int res = INF;
        //在走完str.size()个字母的情况下,当前走到了AC自动机中的第i个位置时的值最小
        for (int i = 0; i <= idx; i++) res = min(res, f[str.size()][i]);
        //如果没有被修改过,返回-1
        if (res == INF) res = -1;
        printf("Case %d: %d\n", T++, res);
    }
    return 0;
}

原文地址:https://www.cnblogs.com/littlehb/p/15748849.html