zoukankan      html  css  js  c++  java
  • 线段树,也挺有趣

    线段树的练习我用了Love_风吟整理的20道题,链接在这 https://www.cnblogs.com/CSU3901130321/p/4192449.html

    其实先把洛谷上面的两道模板题刷了,就会大致了解线段树的原理了。这里我提几句第二道模板题,lazy_plus数组的值一定要记得是后面操作加上的值,意思是,每个数都要加上一个一样的数,当它乘以某个数时,并不意味着每个数加上的值就不等了,这题搞了我几天,做到中期我逐渐忘记了lazy_plus的意思,到后面竟然认为它是一个叠加值,即每个数乘以某个数的叠加,那这么想的话怎么也想不通那题目,也算是我踩到的第一个坑吧,做完这题,理解完,算是对线段树的理解提高了一点。

    废话不多说,其实线段树本身是不难的,所以这次我最想讲的是线段树衍生出来的一些其他操作。

    1 区间合并 (POJ 3667 Hotel和HDU 3397)

    第一道比较适合刚入手,第二道就显得特别的烦了。区间合并要维护较多的数组(特别是第二道),这时候如何防止被搞晕,首先数组名字要起好,其次,要了解它是怎么运行的,https://blog.csdn.net/Diogenes_/article/details/80471916 这个我觉得写得挺好的,但是不代表他的代码就那么的易懂,那么现在我来说说区间合并比较重要的操作,也是最容易搞混的操作吧(以POJ 3667为例)

    int check(int l,int r ,int num)
    {
        if (l==r) return l;
        int mid=(l+r)/2;
        push_down(l,r,num);
        if (continuous_string[num*2]>=place)
        {
            return check(l,mid,num*2);
        }
        else if(you[num*2]+zuo[num*2+1]>=place)
        {
            return mid-you[num*2]+1;
        }
        else if (continuous_string[num*2+1]>=place)
        {
            return check(mid+1,r,num*2+1);
        }
    }
    View Code

    这个函数的作用是查询符合条件的最靠左边的位置。

    首先我们看看这三个if的位置很有讲究,因为题目的要求,我们尽量选择靠左的连续序列,所以我们要按照二叉树的左中右的次序来找答案,如果左边没有,那就找中间,中间没有找右边,以此类推。

    其次我们看看这三个if分别有什么难理解的地方,一开始的时候我想的第一个问题是,中间这个If 里面return 回去的东西怎么可以直接返回一个值,而其他要继续递归下去。仔细一想,发现也不是很难理解。第二个if 找到的值是可以直接确定就是答案的,为什么呢?因为左边找不到答案,找中间的时候,我们是按照左子树上靠右的连续序列个数和右子树上靠右的序列个数相加得到,那么得到的那个值我们可以通过画二叉树来想一下就可以计算得出。其余两个if呢?为什么要递归下去?而不能也像第二个if可以直接得到。其实我们可以这么想,你要从左/右子树去找答案,无非就是要先去看看有没有这样一个符合条件的序列,你有了,我才进去里面看看你是你的左边弄出来的,还是中间弄出来的,或是右边弄出来的。那么按照上面的想法,你从你的左边/右边弄出来,我还要去看看你的左边/右边是怎么弄出来再递归下去,而中间是可以直接得出答案。

    void upgrade(int l,int r,int num)
    {
        int mid=(l+r)/2;
        if (x<=l && r<=y)
        {
            if (operation==2)
            {
                lazy[num]=0;
                continuous_string[num]=r-l+1;
                zuo[num]=r-l+1;
                you[num]=r-l+1;
            }
            else
            {
                lazy[num]=1;
                continuous_string[num]=0;
                zuo[num]=0;
                you[num]=0;
            }
            return;
        }
        push_down(l,r,num);
        if (x<=mid) upgrade(l,mid,num*2);
        if (y>mid) upgrade(mid+1,r,num*2+1);
        push_up(l,r,num);
    }
    View Code

    这个操作是更新现有的二叉树

    其实这个挺好懂的,我讲一下 if (operation==2) 这个操作吧,如果你是操作2,即把房间都清空,那么直接把靠右的连续序列个数和靠左的连续序列个数和这个区间的最大连续序列个数全都赋值为区间长度就好,一开始我做的时候傻×了一下,把zuo[num]=r-l+1 写成了  zuo[num]=mid-l+1 ,把  you[num]=r-l+1 写成了 you[num]=r-mid,错误原因显而易见,我把zuo跟you真理解为左子树和右子树的区间了,导致找错了很久......,zuo区间的意思是这个区间里面靠左的连续序列个数,you区间的意思是这个区间里面靠右的连续序列个数。

    void push_up(int l,int r,int num)
    {
        int mid=(l+r)/2;
        zuo[num]=zuo[num*2];
        you[num]=you[num*2+1];
        if (zuo[num*2]==mid-l+1) zuo[num]+=zuo[num*2+1];
        if (you[num*2+1]==r-mid) you[num]+=you[num*2];
        continuous_string[num]=max(continuous_string[num*2],continuous_string[num*2+1]);
        continuous_string[num]=max(continuous_string[num],you[num*2]+zuo[num*2+1]);    
    }
    View Code

    其实我本以为push_up最容易出错,结果是最省心的,我们直接看到 if (zuo[num*2]==mid-l+1)和 if (you[num*2+1]==r-mid)这两个操作,如果左子树全为连续序列,才能去考虑右子树;如果右子树全为连续序列,才能去考虑左子树。就像人一样,你不把一件事给专心搞好,还想着去做另一件事,做梦去吧。

    PS:区间合并题目,到现在为止,其实大部分都有一个套路,三个基本数组是必备的:区间的左连续长度、右连续长度,合并起来最长的连续长度,其他的另说。

    2 与离散结合(HDU 1542 Atlantics和POJ 1177 矩形的周长并和HDU 1255)

    先讲讲HDU这一题吧,这一题我搞了接近3天,尤为痛苦,无奈之下只能参考别人的做法(https://www.cnblogs.com/scau20110726/archive/2013/03/21/2972808.html),而我发现我把简单问题复杂化了。这题比较简洁的做法就是二分搜离散化的编号,再进行线段树操作,我正因为觉得加上二分做法的话这题可能会超时,于是我写出来一个自认为可以快速搜离散化的编号的方法,结果一直出错。最终在我的不懈努力之下终于AC了。(其实就下面两句代码......,才两句!)

    zuo=lower_bound(pos,pos+m,ss[i].x1)-pos;
    you=lower_bound(pos,pos+m,ss[i].x2)-pos-1;
    View Code

    其实这些题还是挺有做的必要的,有些题没有了传统的push_down和push_up操作,反而更增加了做题人对线段树的理解,再加上离散化的操作,一道具有代表性的题目就这么出来了。

    第二题的话和第一题大同小异,第二题唯一与第一题不太同的地方,就是做法上的小小不同,求横线的长度 = 【现在这次总区间被覆盖的长度和上一次总区间被覆盖的长度之差的绝对值】。

    这个第二题让我弄清楚了我们需要在线段树上维护的是哪一些值,每个区间的值代表什么,再去推,最后得出答案。是不是有点dp的感觉?在我看来,其实这是一种基本的编程思想,就像做数学题一样,为什么你能做出这道大题,除了平时的刷题练习之外,我觉得解题思维是最重要的,思维从哪里来,一是先天的,这个我们不谈;二是后天训练的,那还不是要回归到刷题这个层面上。所以没事多刷题,肯定有好处。

    第三题就又是一种情况,在第一题的基础上再加个数组表示覆盖两次及以上的长度,基本上没很大的区别,倒是想出这么做要花点时间。

    PS:做这几道题,最烦的基本上都是想出如何算出答案这个步骤了,因为这个过程应该算是一道题目的精髓所在。这三道题目大体上代码都一个样,离散的操作,线段的重合等等,但是往往用不同的方法来算出不一样的东西出来。想要变强,这个方面的能力必须要逐渐提升。

    PPS:忽然发现有一个函数叫unique,可以实现去重操作,看来stl库里面有不少宝藏啊......  //11.5

    3 与dp的结合(HDU 4521)

    挺神奇的,之前做的最长上升子序列原来可以用线段树来优化,以后做dp题要留个心了。线段树优化,超时?不存在。(其实也可能会超时的,看每道题的情况)

    总结

    线段树的类型接触的还是有点少,这些真的算是皮毛了,看一下之后一段时间能否做一下高级一点的线段树。

    我的感受

    其实在做这个专题的题目的时候,我是有点痛苦的,因为当我按照自己的想法敲出代码但不尽人意时,往往会去看别人的解题思路来寻求解题方法,但是即使如此,我还是会遇到问题,然后会开始变得烦躁,到最后翻开标程来对着找错,对了就还好,充分理解吸收就过去,但是最令人崩溃的是,自己的程序和标程基本一样,但还是错时,这时候挫败感就会油然而生,而这些错误,往往是一个不起眼的地方,比如像是局部变量全局变量名字搞混的这种小问题。

    为什么我要讲上面这段话?是因为我向我的一位高中朋友反映了一下这些情况,由此我们衍生出来了一些更重要的问题。WA找错,是很难找,那么在打比赛的时候,那该怎么办?如果出来WA这些问题,如何在最短的时间里面挑出来?这令我们陷入了沉思。在ACM的比赛中要三个人一个团队,如果你操手的那一题出现了我上述所说的情况,就是连给你标程,一句一句对着都能一直错的那种,那不就会脱了整个团队的后腿?而这是我们最不想看到的,一是因为是你拖延了时间,二是这是队友信任你才给你操手这一题,你的错误会直接辜负了队友的信任,希望以后这种情况要尽快找到方法解决吧。

  • 相关阅读:
    hdu 5119 Happy Matt Friends
    hdu 5128 The E-pang Palace
    hdu 5131 Song Jiang's rank list
    hdu 5135 Little Zu Chongzhi's Triangles
    hdu 5137 How Many Maos Does the Guanxi Worth
    hdu 5122 K.Bro Sorting
    Human Gene Functions
    Palindrome(最长公共子序列)
    A Simple problem
    Alignment ( 最长上升(下降)子序列 )
  • 原文地址:https://www.cnblogs.com/Y-Knightqin/p/11735654.html
Copyright © 2011-2022 走看看