解题报告:Cube Stacking

解题报告:Cube Stacking

关键字:ACM/ICPC,解题报告,并查集,路径压缩


题目大意 
    有最多30000个标有标号的小方块,输入最多100000次操作,其中每次操作包括以下两种:
        M  x y    将含x的方块堆堆到含y的方块堆上去
        C  x        计算标号x的方块的下方有多少方块

        说明:不会有这样的操作, M x y ,其中x和y在同一堆上。
    要求输出每一次计算C操作的结果。


解题思路

    初看此题像是模拟题,只要模拟30000个方块,并依照它的指示操作,似乎并非很困难。可是,如果有以下的操作(对于n 个方块,t次操作,t>n):

step1        用约n次不到的操作可以将n个方块叠在一个堆上,
step2        用剩余t-n次操作要求堆顶下面含有的方块数

    这样,总共的计算量约有(t-n+1)*n,复杂度约为O(tn),最糟情况有约3e9次计算。对于单case 1000ms来说,很容易超时。

    加速算法的有效手段是用空间换时间,通过预先记录部分方块的值,来避免每次都做重复的计算。首先想到的是记录每一堆的总数,即 a堆有 2 4 5 7;b堆有 1 3 6 ,则记录sum(a)=4 sum(b)=3,那么在通过一次M 4 3(将a堆放在b堆上),c( ai )=i+sum(b) ,c( bi )=i。其中i还是要通过迭代找到元素ai停止来求得。光记录总数是不够的,因为它的最糟情况和上面的最糟情况类似,几乎没有改进。
    因此,可以记录c()来避免多次迭代求i。
    公式改为c( ai )=c( ai )+sum(b) ,为了使堆的表示简化,采用并查集的形式,这样y的代表元素是find_set(y),公式再修改为: (xi表示x的堆中的第i个元素)
        c( xi )=c( xi )+sum(find_set(y))     初始c(i)=0 sum(i)=1


    想法虽好,但实现不好,因为程序需要始终更新c(xi),这个操作同样费事,比如每次对同一个堆上加一块方块,这样更新c(xi)的计算量就是(n-1)*(n-2)/2,是O(n*n),实质上还是平方级的。


    为了替换每次更新的辛苦操作,我们用les[ ]数组记录c(xi)上方的元素个数,可以从下面这个方程了解les的作用:
        c( xi )=sum(find_set(x))-les( xi )     初始c(i)=0 sum(i)=1

    这样,不需要维持一个c数组,只要在执行C操作时,求一下c就可以了。而原本c的维护改为了les的维护。乍看之下,les也不易维护,但结合并查集的路径压缩,不但能灵活更新les数据,并且可以将并查集的路径压缩得很好。

    例如对于 a b c d e f g (左边为堆底,右边为堆顶,即c(f)=5 ,以g为该堆代表)
        现在将 h i j k 堆在其上方,合并操作如下:
            1    les[g]=sum[k]    先更新les[g],a到g元素在查找时边压缩路径边更新
            2    sum[k]+=sum[g]    更新sum[k],因为以k为该堆的代表。

    而find_set()需要实现如下操作:
        当前状态:sum[k]=12        les为在合并操作后的les状态,路径是已压缩
                        a    b    c    d    e    f    g    h    i    j    k
        parent    g    g    g    g    g    g    k    k    k    k    k
        les         6    5    4    3    2    1    4    3    2    1    0

        例如find_set(a), 则因为递归会调用find_set(f),更新les[ f ]=les[g] +les[f]=5,并压缩f,f 更新完毕;返回到find_set(h),更新les[ h ]=les[g]+les[h]=6,并压缩h。如此反复,便可以得到:
                        a    b    c    d    e    f    g    h    i    j    k
        parent    k    k    k    k    k    k    k    k    k    k    k
        les        10    9    8    7    6    5    4    3    2    1    0

        虽然用到了递归,但是深度是随着 find_set 的调用而减少直至2层,所以只要经常调用 find_set 路径就可以很快得压缩了,那么递归的消耗根本不算什么。

    有了les和sum的计算,所求的c就可以通过 c=sum-les-1 (减一是因为我取堆顶les为0,如果要避免减一则需要在sum的更新和les初始时做些改动,其实是无碍的)


解题程序

C++程序 Mem: 440K     Time:169MS

                    #define MAX 30050                            //    根据题意,最多30000个方块
                    int p[MAX], sum[MAX],les[MAX];        //    p记录集合代表,sum和les辅助计算

                    void init() {                                            //    初始化工作
                        for (int i = 0; i < MAX; i++) {
                            p[i] = i;    sum[i]=1;    les[i]=0;
                        }
                    }  

                    void link(int x, int y) {                            //    连接两个集合
                        p[y] = x;    les[y]=sum[x];    sum[x]+=sum[y];   
                    }

                    int getles(int top,int c){                        //    压缩路径,更新les数组
                        if(p[c]!=top){
                            les[c]+=getles(top,p[c]);            //    更新c的les值
                            p[c]=top;                                        //    压缩路径
                        }
                        return les[c];
                    }

                    int find_set(int d) {                    //    查找元素在集合的代表
                        int t=p[d];
                       if(d!=p[d]){
                            t=find_set(p[d]);
                            getles(t,d);                        //    查找的同时压缩路径
                        }
                        return p[d];                        //    因为压缩了,所以p[d]才是集合代表
                    }

                    void union_set(int x, int y) {                //    合并两个集合
                        link(find_set(x),find_set(y));
                    }

                    int main(){
                        int p,x,y;
                        char op;
                        scanf("%d",&p);                    //    输入操作数目
                        init();
                        while(p--){
                            scanf("/n%c",&op);            //    输入操作
                            switch(op){
                                case 'M':               
                                    scanf("%d%d",&x,&y);
                                    union_set(x,y);            //    合并堆操作
                                    break;
                                case 'C':
                                    scanf("%d",&x);
                                    printf("%d/n",sum[find_set(x)]-les[x]-1);        //    输出c操作
                                    break;
                            }
                        }
                    }


心得体会

    这是一道比较好的关于数据结构的题目,它的主要考点是并查集的概念。但是能够想到并查集并不能帮助我们解题,因为题目的要求比简单的并查集要复杂得多。为了解决这道题目,需要修改并查集的union_set和find_set两个基本操作,而在find_set中如果知道路径压缩的方法便能更好得解决这道题目了。

    通过这题的练习,我学到了两点,其一学习数据结构不能只学习表面,需要深入才能应用自如;其二是针对不同的问题要善于修改一些常用的结构和工具,以灵活应付各种题型对于解题是相当有好处的。

相关链接

Pku1988:
    http://acm.pku.edu.cn/JudgeOnline/showproblem?problem_id=1988
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值