• JZOJ5822 【NOIP提高A组模拟2018.8.16】 量子纠缠


    这是一道很巧妙的题目。
    今早,我调了好久,终于将它切掉了……


    题目

    Description

    Description

    Input

    第一行包含一个正整数 m,代表操作数。
    接下来 m 行,每行可能有以下形式:
    1 s 代表将数字串 s 加入信息集中
    2 s 代表询问数字串 s 是否在信息集中
    3 a b 代表使数字串 a 和 b 互相纠缠

    Output

    对于每一个 2 操作,如果询问串不在集合中,请输出一行一个整数 0,否则输出一行一个整 数 1。

    Sample Input

    11
    1 123
    2 123
    2 0
    3 12 13
    1 124
    2 133
    2 134
    2 13
    3 1 11
    2 111
    2 11111111111111111111111124

    Sample Output

    1
    0
    1
    1
    0
    0
    1

    Data constraint

    Data constraint

    题目大意是这样的:
    维护一个数字串集合,每一次有三个操作:
    1. 插入一个数字串。
    2. 询问一个数字串是否在集合中。
    3. 纠缠两个数字串(不一定在这些数字串之内),假设这两个数字串分别为AB,插入一些数字串,使得对于任意集合中的A+C,都有B+C也在集合中;对于任意集合中的B+D,都有A+D在集合中(有可能纠缠过后会加入无限的数字串)。

    解题思路

    我才不会告诉你,我一开始看到这题时,没有理解题目大意。我以为这些加入集合的字符串全部合并在了一起,一想到可能要用某些高端的字符串数据结构,我就感到害怕,于是果断地弃掉了这题。
    显然,如果只有前两个操作,那么这就是一道裸裸的Trie。
    为什么?不知道为什么的同志们,请再次仔细地理解一下题目大意。如果还不知道为什么,就查查Trie到底是个什么东西。说真的,我觉得Trie是一种无师自通的玩意儿。只要你知道Trie是个什么东西,那么前两个操作对于你来说一定不难。
    现在的问题是如何搞第三个操作。
    既然前两个操作都和Trie建立了联系,那么不妨想想,如何用Trie来解决。
    我们在看看所谓“纠缠”的条件。什么是“纠缠”?把Trie上表示AB点找出来,设为ab
    那么我们是不是可以理解成,ab为根的子树,长得一模一样?
    可能有些不好想象,感受一下~~~
    算了,放张图。

    同化
    这样可以理解了吗?
    就是让以它们为根的子树长得一模一样。
    所以有个思路就是,先把ab找出来,暴力地同化它们的子树。
    诶~等等,似乎会超时。
    读者:你在逗我?
    暴力同化它们的子树,这是不现实的。不仅暴力同化花很多时间空间,而且在以后,当有节点插入时,还要在搞,比蜗牛还慢……
    与其这样,不如合并ab。这样子,下面的就变得一模一样。当后面有插入时,那也可以直接修改,反正两个已经合为一体了。
    这个方法非常神奇。显然,它也是正确的。
    可能你们会有一堆问题吧,比如,如何合并?
    合并
    先将ab找出来,然后合并。再递归它们的儿子,分别合并。(用并查集)
    这样的时间复杂度是不会炸的。建Trie所需要的节点是有限的。在没合并之前,它们就是互相独立的块。每次的合并,将会减少一个块。下次再访问它们时,它们已经是一体的了。
    无限的情况怎么办?
    其实无限的情况是没必要特判的。相信聪明的读者已经发现了,实际上,出现无限的情况,也就是在合并的时候一个节点和它的祖先合并。那么这些Trie上的转移边,实际上就变成了一个有向图。查询的时候,就像以前那样查询就行了。
    一棵好好的Trie树,经过各种合并后,形成了一个有向图……是不是很神奇呢?

    算法流程

    前面的两个操作就不说了,直接将第三个操作的核心——合并。
    假设现在要合并以ab为根的子树。
    首先,将ab各自跳到getfather
    如果a=b则退出。
    a所在的集合,并入b所在的集合中。用a尝试更新b上的have。(表示这个节点是否有字符串)
    然后,枚举它们的儿子。
    1. 如果ab都没有儿子,不理它们。
    2. 如果a有儿子,而b没有儿子,那么,将b的这个转移边连过去。
    3. 如果a没有儿子,b有儿子,不理它们(因为是用a合并到b中)。
    4. 如果两个都有儿子,那么递归合并它们的儿子,合并完后,b跳到它的getfather(这一条很玄学,某大佬的解释是:有可能合并完它们的儿子之后,会对已经和b同一个并查集中的节点有影响,因为在各种合并之后,这不再是一棵树,而是有向图)。

    代码

    千言万语不如一标程。

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    struct Trie
    {
        int c[10];//转移边
        bool have;//表示这点表示的字符串是否存在于集合之中
        int n;//这个节点所在的并查集编号
        int num(); //num()就相当于平常打的getfather。在后面使用这些节点时,都是用它们的getfather(),可以看做一个编号。
    } d[10000000];
    int null,cnt,root;
    int Trie::num() {if (&d[n]==this) return n;return n=d[n].num();}
    int newnode() {++cnt;d[cnt].n=cnt;return cnt;}
    void insert(char *s)
    {
        int t=root;
        for (;*s!='';++s)
        {
            if (!d[t].c[*s-'0'])
                d[t].c[*s-'0']=newnode();
            t=d[d[t].c[*s-'0']].num();
        }
        d[t].have=1;
    }
    bool find(char *s)
    {
        int t=root;
        for (;*s!='';++s)
        {
            if (!d[t].c[*s-'0'])
                return 0;
            t=d[d[t].c[*s-'0']].num();
        }
        return d[t].have;
    }
    int open(char *s)//在Trie中开辟出数字串s。
    {
        int t=root;
        for (;*s!='';++s)
        {
            if (!d[t].c[*s-'0'])
                d[t].c[*s-'0']=newnode();
            t=d[d[t].c[*s-'0']].num();
        }
        return t;
    }
    void merge(int t1,int t2)
    {
        //这里就不用getfather()了,因为在之前我们已经能够保证它们在并查集中一定是最高的
        if (t1==t2)
            return;
        d[t1].n=t2;//合并
        d[t2].have|=d[t1].have;//试着更新have
        for (int i=0;i<10;++i)
        {
            int son1=d[d[t1].c[i]].num(),son2=d[d[t2].c[i]].num();
            if (son1)
            {
                if (!son2)
                    d[t2].c[i]=son1;//如果t1有儿子,t2没儿子,就将转移边连过来。
                else 
                {
                    merge(son1,son2);//递归合并
                    t2=d[t2].num();//试着更新t2,因为下面的合并有可能影响到t2
                }
            }
        }
    }
    char str1[8000001],str2[100];
    int main()
    {
        freopen("quantum.in","r",stdin);
        freopen("quantum.out","w",stdout);
        null=0;
        root=newnode();
        int T;
        scanf("%d",&T);
        while (T--)
        {
            int op;
            scanf("%d",&op);
            if (op==1)
            {
                scanf("%s",str1);
                insert(str1);
            }
            else if (op==2)
            {
                scanf("%s",str1);
                printf("%d
    ",find(str1));
            }
            else
            {
                scanf("%s %s",str1,str2);
                merge(open(str1),open(str2));
            }
        }
        return 0;
    }

    总结

    这题真的很神奇。
    一个好好的Trie,因为各种奇奇怪怪的合并操作将树压成了一个无向图。
    注意,上面的t2=d[t2].num();这一句话非常的玄学,不好理解,要特别注意。这就是60分和100分的差距。

  • 相关阅读:
    关于在n-1的数组中找到那个被减去的数及异或与位与
    oracle 递归查询
    EL,OGNL表达式 $ % #
    TypeError: record.get is not a function
    sql添加、查询小错误
    Unity中操作手机常用功能
    Unity 中PlayerPrefs类实现数据本地化存储
    unity3d 下操作excel 与打印
    unity中读写二进制文件
    List排序问题
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145280.html
Copyright © 2020-2023  润新知