敌兵布阵 HDU - 1166

看了一下午+一上午+一下午的 线段树 。   再看着别人的代码打了一下午。 终于过了, 没有系统的学习,让我有点举步维艰的感觉。 反正就坚持下去把,然后不停的寻找新的机会和方法。第一次做线段树,感觉很吃力,不过慢慢刷下来自然会熟悉题型,知道该怎么做。加油!下面我会尽可能的给出注释,一来如果有人看到这篇文章的话,可以理解我的思维,而来我也可以认认真真的分析好每一个函数,每一句话这么写的原因,也加深自己对线段树的理解。


#include <stdio.h>

#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;

int ans=0;

struct node
{
    int l,r,v;  // 存储边界,那么以后寻找该节点对应的区间只要知道编号就可以了。 l代表left,r代表right ,v代表你要存的价值
}sgetree[200000+1000]; // 理论上如果区间内有n个数,那么对应的节点数应该是2^n-1 就够了,看到别人博客都是开4n;
int a[500000+1000];// 这块我感觉数据可能不止5w . 开5w+10就tle ,开50w 就AC 。
char str[100];

// Add函数,把区间(下标)内某个节点的v改变,那么是不是父亲节点,父亲的父亲节点,xxxxxxxx的父亲节点一直到根节点的和是不是一直都要受到影响呢??那么我们怎么样才能把这个影响给消除掉呢(解决问题)? 那我们必须要找到这个点,才从这个点往上一直递归,这样子是不是就把改变数据带来的影响给消除了呢?仔细思考一下是的啊! 哈哈。。。好我们老老实实说题目好吧。 做题更重要!毕竟我很蠢啊。。       首先 由main 函数里函数调用传输  index , add 和1  为什么每次要传输1呢 ?  是因为我每次递归的时候,我的儿子节点和我当前的编号有关系,就是2*n+1和2*n的关系了。   那么我先判断终止条件,就是找到对应的index 满足的要求呢,就是l=r=index  对吧!因为每个点代表一个区间,而改最底层的叶子节点就一个数,就代表该下标的,其实这就是规律吧,可以这么理解,因为线段树最终分分分,一直分就会分成只有一个点的区间情况,不可能有重复的单区间,所以,就是这样子了。  那么OK,我们判断完了终止条件,那么终止后我们要做什么呢,就是把值改变,再不变等回溯了就没机会了,所以必须得改变。 上面说了,用递归,用递归这就是构建线段树的关键,线段树很多操作都是递归递归做。 现在我们看如何递归到底层并且把值改变 ,消除改变值带来上面的改变。 递归操作,其实和建树的过程是有相似点的,我们拿到一个下标(一个点的区间)我们要找,肯定先找范围大的,其实这和二分很像,我先看一半的情况,index 会属于左半棵树呢还是右半颗树呢,然后确定了之后再确定是左半颗呢还是右半颗呢,直到最后确定下来。 确定下来后return  返回到它的上一层,那么我再更新父节点的value=左儿子的v+右儿子的v,怎么找到左右儿子,就凭父节点的sgetree的下标和左右儿子的关系来做。 那么我就一路更新回去,直到更新到 根节点。

总结:对单节点进行数据更新,会影响上面所要的操作,那么我们就要通过递归,判断index 和  mid 的关系找到该节点的位置,并且回溯更新值。 那么我相信肯定有人会有疑惑了,为什么不直接把segtree[下标index]给改变呢。 如果是这样,就很难把当前叶子节点改变的影响传递到父亲节点了,所以需要递归去寻找该叶子节点(而不是直接改),递归完后又回溯再做  父亲节点的v 和 左右儿子的关系, 那么这样一层一层上去就可以完成update的任务了,其实这和建树的过程是一样的,只不过通过index的关系,不停的缩小一半一半一半的范围,最终找到节点的l=r=index的情况才会停止。

void Add(int index,int add,int root)
{
    if(sgetree[root].l==index && index==sgetree[root].r)
    {
        sgetree[root].v+=add; return ;
    }
    int mid=(sgetree[root].l+sgetree[root].r)/2;
    if(index > mid)
        Add(index,add,root*2+1);
    else
        Add(index,add,root*2);
    sgetree[root].v=sgetree[root*2].v+sgetree[root*2+1].v;
}

 // 建树build函数,原理是不停的递归,递归完然后回溯保存需要的值。递归函数一定要有终止条件,否则就是死循环。那么终止条件应该是什么呢?根据二叉树的性质,到最后一层的时候就应该终止,其对应的left,right相等,那么该节点就该截止了,因为无法继续分下去了,那么这个节点就代表了这个点(区间长度为1),这个时候left=right,就是你要存储数据的下标了。那么我们就return ;返回到调用它的上一层。     当递归到最后一层时候  语句③ →  语句① →语句② → 语句④ 那么我们就成功的保存了一颗小树的最小值,函数运行到最后一句话,又返回到上一层调用它的地方回到① → ② 一直这么下去,小树慢慢长大变成半颗大树后 ; 又递归的右边的树,然后又重复半颗大树从顶层递归到底层,然后从底层回溯到顶层,每个节点保存儿子的和,一层一层推上去,最后每个节点结构体对应一个left right 同时得到了  [left~right] 的 最小值。 我们可以发现这样递归,树的左右儿子和父亲节点编号存在一定关系,哇那真是太棒了(虽然函数写的时候就这样了)。但便于理解,正是由于有这样良好的规律,我们可以很方便的从顶层遍历到底层,并且在执行一些操作的时候可以排除掉一些情况你就不用去搜索了,否则如果暴力就要花很多时间,或许这就是logn 的原因吧。 

总结: 对于建树的过程中,要传递的参数有区间范围,即left right,还有下一个子节点的sgetree 编号。对于建树过程中,确定终止条件为l=r,并且赋值,再回溯保存下你要求的值(对于这题是求和)。

void build(int l,int r,int root)  
{
    sgetree[root].l=l;  //  不管是不是叶子节点还是非叶子节点,我都要记录,所以把这个语句放在函数最前面
    sgetree[root].r=r;
    if(l==r) ③
    {
        sgetree[root].v=a[l];
        return ;
    }
    else
    {
        int mid=(l+r)/2;
        build(l,mid,root*2);    ①
        build(mid+1,r,root*2+1);   ②
        sgetree[root].v=sgetree[root*2].v+sgetree[root*2+1].v; ④
    }
}

//求区间[l,f]的和,同样这和上面的所有操作都一样,我都需要在main函数调用query 时候 传入 1,然后自顶向下走一圈再自顶向上,再自顶向下,再自底向上走。  依旧是递归,从顶层往下递归,这是线段树的优势,因为线段树操作的时候过滤了无作用的区间。 继续说query 函数 操作 原理。 我要询问区间  l~j  下标的区间和。 这个递归怎么处理比较合适呢? 我们从简单的入手,  对于第一次寻找时候,如果区间在1~mid 或者 mid+1~n的情况 那么我们就舍弃另一边,因此就有了语句①的操作。如果left<mid && right>mid 的情况下那么我们把所求的区间和分成2部分来求, 一部分是 left~ mid   &&   mid+1~right 求 解这两个区间的和,原因是因为我节点区间对应分配的过程中,是按这样划分的。所以这样做可以求,那么我们继续往下一层递归,最差的情况就是我把每一个都拆分成了单位长度的区间,然后sgetree[root].l=l ,  sgetree[root].r=r 那么我们在前面建树的过程中已经保存了该节点的和,那么直接ans+=sgetree[root].v 那么就完成了, 过程中也可能在某个区间(除了单位长度的区间)这是我们希望得到的,可以缩短递归的深度,已经到了getree[root].l=l ,  sgetree[root].r=r 的情况就直接返回,那么就可以写出下面的 query 函数。            

总结思路:  对于区间求和,  判断当前所求区间left,right和已知区间mid的范围关系, 把区间和拆成多个小区间的和,共有3种情况(见if else 语句)。 对于每一个子区间,又有如上的关系,直到递归到情况sgetree[root].l==l && sgetree[root].r==r  ,,  说明不用再往下递归了,直接ans+=sgetree[root].v 就是和, 那么把一个区间拆成很多个小区间来求和存储在ans 中,并且在过程中省略掉一些不可能的区间,就可以缩短很多的时间,对于n的范围很大的情况下,可以节省了很多时间,因为不需要遍历每一个元素。   只是把子问题保存在结果中,用递归找到子问题的结果, 这个将起来和动态规划有点相似,动态规划是把每一个子问题的最优解都保存下来,不管用不用到,都先保存下来,便于后面的递推。 哇  就这样了,接下来继续做题,然后好好的总结感悟 , 努力努力努力!

void query(int l,int r,int root) 
{
    //printf("%d-",ans);
    if(sgetree[root].l==l && sgetree[root].r==r)
    {
        ans+=sgetree[root].v;
        //printf("%d-",ans);
        return ;
    }
    int mid=(sgetree[root].l+sgetree[root].r)/2;    ①
    if(r <= mid) query(l,r,root*2);                 ①
    else if(l>=mid+1) query(l,r,root*2+1);            ①
    else
    {
        query(l,mid,root*2);
        query(mid+1,r,root*2+1);
    }
}


int main(void)
{
    int t;
    int n;
    cin >> t;
    for(int kase=1;kase<=t;kase++)
    {
        cin >> n;
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]); // 在输入数量比较多的情况下,最好不要用 cin  输入 , 容易tle 或者跑的比较慢,为了保险还是 这么做;
        printf("Case %d:\n",kase);
        /*for(int i=1;i<=n;i++) //  最好的习惯是检查输入的数据有没有出错,如果输入错误,那么全部OVER,而且极有可能你会找不出BUG。
            printf("%d-",a[i]);*/
        build(1,n,1);  // 建立树的过程,写建树的函数
        while((scanf("%s",str))==1)
        {
            ans=0;
            if(strcmp(str,"End")==0) break; //写成return 0 结果只能返回一次,得牢记break 和 return 的区别,写代码一定要逻辑清楚。
            else if(strcmp(str,"Add")==0) //如果是某个点加上某个值
            {
                int index,add; // 记录下标,和加多少
                cin >> index >> add;
                Add(index,add,1); //写 Add函数 , 传输下标,加多少,以及从1开始
            }
            else if(strcmp(str,"Sub")==0)
            {
                int index,add;
                cin >> index >> add;
                Add(index,-add,1);
            }
            else if(strcmp(str,"Query")==0)
            {
                int x,y;
                cin >> x >> y;
                query(x,y,1);
                printf("%d\n",ans);
            }
        }
    }

}


总结感悟: 这或许是我写的最认真的一篇感悟了,也许是意识到自己太弱了的原因,所以要好好对待每一题,我为我上一年的碌碌无为感到羞耻。

对于线段树问题,最关键的 问题就是去递归下去,这和搜索很像,对线段树的操作几乎都是通过递归完成遍历。其实线段树很暴力的写法,但是是一种合理范围内的暴力。因此。 学好递归,线段树就没什么问题了,最重要的是一个逻辑思维的过程,分析。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务