zoukankan      html  css  js  c++  java
  • 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;
    }
    
    
  • 相关阅读:
    PageControl1
    IE6 发送奇数个汉字出现的BUG机器解决之道
    十 C# Socket协议的形象描述
    深入浅出URL编码
    认识session
    Servlet过滤器介绍之原理分析
    web开发标准
    JS 截取字符串及字符与数字转换
    Servet和JSP学习总结(转载)
    端口、系统服务、系统进程概念
  • 原文地址:https://www.cnblogs.com/littlehb/p/15748849.html
Copyright © 2011-2022 走看看