洛谷P2444 病毒(AC自动机变形)

这道题目要求构造一个无限长的文本串,使得其中没有模式串出现,利用AC自动机原理,通过单向边和失配指针寻找不含危险标记的环。通过DFS寻找环并避免危险标记,优化失配指针构建。解题过程中需要注意路径标记和历史访问记录,以及避免误判环的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作为一道蹂躏了我一天的题…我也没啥好说的了QAQ…
思路概述
我们知道,AC自动机是一种多模字符串匹配算法。构造 Trie 树 后,在模式串末尾一位的结点作上标记。平常的 AC自动机 是尽量能多接触到这些标记,使总值最大。本题倒是有点奇葩,要构造一个可行的无限长文本串,使没有任何子串为给出模式串中的一个。也就是说,我们需要让平常 AC自动机 的查询操作,尽量避免标记,能用失配指针跳转就跳转。

因为要有无限长的可行串,根据 AC自动机 的原理,我们可以将结点连接到儿子的边当作一条单向边,同时失配指针也当作一条单向边,如果存在一个环,且环上没有任何危险标记(即病毒代码段末尾一位的结点专门作的编号),此时 AC自动机 能一直在环上匹配,并且永远也不会得到为模式串的一个子串,就像程序中的死循环一样。这个找环我们可以通过 dfs 来实现。

注意事项
1 . 我们需要建立两个布尔数组,其中一个布尔数组记录每个节点在当前 dfs 走的路径上有没有被选中,另一个布尔数组记录每个节点历史上有没有被访问过。如果当前路径形成回路,就找到环了,应该还是比较好实现的。
2 . 避免危险标记,也就是说如果下一个结点拥有危险标记,就不走那个结点。
3 . 在构造失配指针时,一个很明显的优化是:如果一个结点拥有了失配指针,它指向的结点如果有危险标记,自己必然也危险,因为它到根结点形成的串是自己到根节点的后缀。
4.为什么要两个bool标记?因为如图1,如果不能保证当前dfs时在sta中的节点是链式结构的话,就会可能出现误判为环的可能。
5.为什么要在造指针的时候做优化,除了时间上的考虑似乎还有别的因素…如图吧。否则的话,如果只访问子节点,那永远成不了环。访问fail节点的话,又可能自环。
这里写图片描述

代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
struct node{
    node*next1[2];
    node*fail;
    bool tag,vis,insta;//tag标记是否为单词结尾,vis表示此节点是否被访问过,insta表示此节点是否正在被访问的链上
    node(){
        memset(next1,0,sizeof(next1));fail=NULL;tag=vis=insta=false;
    }
}*root=new node();
int cnt=0;
void insert(string str)
{
    node*p=root;
    for(int i=0;i<str.size();i++){
        int x=str[i]-'0';
        if(!p->next1[x]){
            node*t=new node();p->next1[x]=t;
        }
        p=p->next1[x];
    }
    p->tag=true;
}
void build_fail_pointer(void)
{
    node*p,*temp;
    queue<node*>que;que.push(root);
    while(!que.empty()){
        temp=que.front();que.pop();
        for(int i=0;i<2;i++){
            if(temp->next1[i]){
                if(temp==root){
                    temp->next1[i]->fail=root;
                }
                else{
                    p=temp->fail;
                    while(p){
                        if(p->next1[i]){
                            temp->next1[i]->fail=p->next1[i];
                            if(p->next1[i]->tag)temp->next1[i]->tag=true;//如果指向的失配节点是结尾,考虑到失配节点到root的过程中
                            //是一个单词,那么拥有相同后缀的原节点自然也是结尾.优化要放在这里否则可能出现遗漏
                            break;
                        }
                        p=p->fail;
                    }
                    if(!p)temp->next1[i]->fail=root;
                }
                que.push(temp->next1[i]);
            }
            else temp->next1[i]=temp->fail->next1[i];//优化,如果没有下一个节点,那么该节点就指向失配指针的对应节点
            //不过这个优化放在有ac_automation函数的题目中似乎会re,具体原因暂时不明
        }
    }
}
void dfs(node*p)
{
    p->vis=p->insta=true;
//    if(p->fail&&p->fail->tag){
//        p->insta=false;return;
//    }
    if(p->next1[0]){
        if(p->next1[0]->insta){//这就解决了所有在sta中的节点都被访问过而不能再访问的问题,确保不会重复访问而且可以正确判断成环
            cout<<"TAK"<<endl;exit(0);
        }
        if(!p->next1[0]->tag&&!p->next1[0]->vis)
            dfs(p->next1[0]);
    }
    if(p->next1[1]){
        if(p->next1[1]->insta){
            cout<<"TAK"<<endl;exit(0);
        }
        if(!p->next1[1]->tag&&!p->next1[1]->vis)
            dfs(p->next1[1]);
    }
    p->insta=false;
}
int main()
{
    int n,i,j;string str;
    cin>>n;
    for(i=1;i<=n;i++){
        cin>>str;insert(str);
    }
    build_fail_pointer();
    dfs(root);
    cout<<"NIE"<<endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值