小小线段树 以HDU-1166 敌兵布阵为例

【异世界情绪】日文翻唱《鳥の詩/鸟之诗》
引个流,情绪的鸟之诗真的好听,听着这个学习效率巨高!


前言

说到线段树,就很难不想到那该死的build,add和query函数,完全不懂谁是用来干嘛的啦。而其他人写的博客对我来说不是那么容易理解,所以我在这里把这几天学到的关于线段树的知识记录下来。要是以后突然不懂了还能还看看,当然也方便像现在的我一样的正在学习算法的小白。

一、build函数

如其字面所示,build函数就是用来建树的,那么一般建树要干嘛呢?
这里就得先谈一下,tree一般有哪些元素了。当然,为了方便,一般会用一下struct。这里我们用一道题(HDU-1166)来说明。
在这里插入图片描述
在这里插入图片描述

/*那么我们读完题,这一颗树会有什么元素呢?
 *首先,一颗树应该有其左右子树的下标,如果左右下标相等,那么此时为叶子
 *即没有左右子树了
 *然后,这棵树应该还有用来记录区间总人数的元素sum
 *当然,我们应该还要写下一个记录营地人数的date[i],i指代某个营地
 *所以我们写下这样一个函数体
 **/
 #include <bits/stdc++.h>//直接万能库!( '▿ ' )
 using namespace std;
 const long long maxn=5e5+5;
 long long date[maxn];
 struct Tree
 {
   long long l,r;
   long long sum;
 }tree[maxn*4];/*为什么要开4倍空间呢?我给忘了,大家可以自己去查查
                *我之后也会去查查,再来更新这里的
                */
//现在为build函数做完了准备工作,我们现在来写build!
void build(int l,int r,int i)/*l和r代表想从l到r建立树,i决定了谁来做根
                              *或者说从i开始
                              *写完build给大家演示一下①*/
{
     tree[i].l=l;
     tree[i].r=r;//更新i坐标的左右下标
     if(l==r){tree[i].sum=date[l];return;}//既然是叶子节点了,那么此时这
                            //个节点的角色就是i营地了,应当记录i营地的人数
     //l和r不相等呢?该建立左右子树
       long long mid=(l+r)>>1;//这里是右移1位,在二进制的世界了相当于/2
       //即和long long mid=(l+r)/2等价
       build(l,mid,i<<1);//左子树
       build(mid+1,r,i<<1|1);//右子树
       //说明左右子树的下标规定为:其根的下标的2倍和2倍+1
       //即左子树li=i<<1,右子树ri=i<<1|1
       //现在跟新一下sum
       //上一节点的sum等于其左右子树的sum的和,也会演示一下②
       tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum;
}

演示①和②:

突然发现演示①、②可以用一个例子来同时说明!
演示用代码:

int main()
{
    int T;
    cin>>T;
    for(int x=1;x<=T;x++)
    {
        int N;
        cin>>N;
        for(int y=1;y<=N;y++)
        {
            cin>>date[y];
        }
        build(1,N,1);//修改处,只需修改i;
        for(int i=1;i<=128;i++)
        {
            printf("%d: %d\n",i,tree[i].sum);
        }
    }
    return 0;
}

在这里插入图片描述这里写开来是这样的:
在这里插入图片描述不难发现sum是如何被一步步确定的,这个我实在不知道怎么掰开揉碎了给你讲了,你要这都看不懂,听我一句劝,360行,行行出状元。另择行业从事吧,你不适合编程。
划红线的地方的sum,但是此时他们都是叶子,没有左右儿子了,所以sum=date[i]。当然在图上我们发现,叶子的值的顺序正好是我们输入date[i]的顺序。但是他们的下标却不一样,是为什么呢?
在这里插入图片描述为什么不一样呢?我们从对上面这副图进行推到,得到了这么一副图,于是我们能够明白tree[i]的下标是用来确定其在树中的位置的,与date[i]的下标无关。(为了接下来的说明方便、清楚,我们让tree[i]的i变为ti即tree[ti],同理date[i]变为date[di])
那么,ti和di有什么关系呢?ti和di的关系是由谁决定的呢?我们来看图中划红线的地方,不难发现此时的ti和我们输入的顺序相同,即此时ti=di。那么我们就返回观察build函数的代码发现:

if(l==r){tree[i].sum=date[l];return;}
long long mid=(l+r)>>1;
build(l,mid,i<<1);//左子树
build(mid+1,r,i<<1|1);//右子树

这几行代码起了关键作用,首先是第2行的mid,它使得当l!=r时左右子树的r和l分别得到了改变,从而达到了在不断的递归中使得l和r逐渐相等。并最终在第1行的if判断下使ti和di形成了联系。从而让相应位置的ti叶子存下了di的date的值。

如果改成build(1,N,2)
变为:
在这里插入图片描述

二、add函数

顾名思义add函数是用来给树里的东西加点什么的,当然作用还是取决于符号±嘿嘿~
那么在本题里,为了方便我们就通过符号来偷个懒,让add一个函数完成Add和Sub的命令吧。如下是代码:

void Add(int i,int j,int xb)//为什么题目说明明只有i,j2个参数传入我们
//还设置一个xb(下标)传入干什么?这个啊,我们之后再说( '▿ ' )
{
  //如果下标为xb的l和r都等于i的话,此时xb其实就等于i
  //此时我们发现,噢!原来xb是用来和i做比较的
  //if(tree[xb].l==i&&tree[xb].r==i){tree[xb]+=j;return ;}
  //但是为了好看一点,我们改写成:
  if(tree[xb].l==tree[xb].r){tree[xb].sum+=j;return ;}
  //我们接着来看
  else
  {
    //相等的说完了,该考虑考虑不等的情况了
    //不等说明此时不是叶子,所以我们先更新一下他的sum
    tree[xb].sum+=j;
    //这个时候可能就有同学要问了,啊你这个不应该不要嘛!
    //你都在上面的if更新了,你这里还+j干嘛!数据会不准的
    //可是,我们仔细想想,如果我们不更新的话,谁来更新呢?
    //要知道我们只建了一次树,如果想不写这一行的话,就代表着我们要不断
    //重新建树,而且这个操作还会很麻烦,所以我们要同时更新叶子的上级的sum
    //接着就是常规操作,利用递归来更新左右儿子,直到递归到叶子
    //要是i大于左儿子的r说明i在tree[zb]的r区间,即右儿子那边
    if(i>tree[xb<<1].r)Add(i,j,xb<<1|1);//这里也有演示③
    //不然就在左儿子那边
    else Add(i,j,xb<<1);
  }
}

演示③:

在这里插入图片描述

三、query函数

终于到了query(查询)函数了,淦,累死了( '▿ ’ )
这个函数式用来在i到j区间查询sum的,代码如下

int Query(int i,int j,int xb)//为什么是三个参数就不用我来说了吧( '▿ ' )
{
  //如果区间(tree[xb].l,tree[xb].r)等于区间(i,j)的话
  //直接就返回tree[xb].sum就可以了
  int mid=(tree[xb].l+tree[xb].r)>>1;//之后mid有用,自己看看( '▿ ' )
  if(tree[xb].l==i&&tree[xb].r==j)return tree[xb].sum;
  //要是不等就有三种情况,我们来看:
  else
  {
    //第一种,i>mid,那么ij在mid到tree[xb].r之间,即右子树嘛( '▿ ' )
    if(i>mid)return Query(i,j,xb<<1|1);
    //第二种,j<=mid,在左子树那边
    else if(j<=mid)return Query(i,j,xb<<1);
    //否则就是第三种,他脚踏两艘船,两边都有
    else return Query(i,mid,xb<<1)+Query(mid+1,j,xb<<1|1);
  }
}

于是,至此我们三个函数都写完了,该写main了!

四、最终的main函数

直接上代码!

int main()
{
  int T;
  cin>>T;
  for(int x=1;x<=T;x++)
  {
    int N,i,j;
    cin>>N;
    memset(date,0,sizeof(date));///每次都要记得给date归零!
    for(int j=1;j<=N;j++)
       cin>>date[j];
    build(1,N,1);//建树,从1到N,下标1在最上面为顶
    cout<<"Case "<<x<<":"<<endl;
    string s;
    while(cin>>s)
    {
      if(s=="End")break;
      cin>>i>>j;
      if(s=="Add")Add(i,j,1);//从1这个顶开始加
      if(s=="Sub")Add(i,-j,1);//同上
      if(s=="Query")cout<<Query(i,j,1)<<endl;
    }
  }
  return 0;
}

至此,我们终于写完了这道题,也基本讲明白了线段树是个啥。
开心!( '▿ ’ )

总结

说一下,我这个main是C++风格的,你要是直接拿走在本地是能过的,但oj上是会超时的,自己改成C风格的吧。写完了,开心~!( '▿ ’ )
然后,快去听情绪的《鳥の詩/鸟之诗》!

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Description C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了。A国在海岸线沿直线布置了N个工营地,Derek和Tidy的任务就是要监视这些工营地的活动情况。由于采取了某种先进的监测手段,所以每个工营地的人数C国都掌握的一清二楚,每个工营地的人数都有可能发生变动,可能增加或减少若干人手,但这些都逃不过C国的监视。 中央情报局要研究人究竟演习什么战术,所以Tidy要随时向Derek汇报某一段连续的工营地一共有多少人,如Derek问:“Tidy,马上汇报第3个营地到第10个营地共有多少人!”Tidy就要马上开始计算这一段的总人数并汇报。但营地的人数经常变动,而Derek每次询问的段都不一样,所以Tidy不得不每次都一个一个营地的去数,很快就精疲力尽了,Derek对Tidy的计算速度越来越不满:"你个死肥仔,算得这么慢,我炒你鱿鱼!”Tidy想:“你自己来算算看,这可真是一项累人的工作!我恨不得你炒我鱿鱼呢!”无奈之下,Tidy只好打电话向计算机专家Windbreaker求救,Windbreaker说:“死肥仔,叫你平时做多点acm题和看多点算法书,现在尝到苦果了吧!”Tidy说:"我知错了。。。"但Windbreaker已经挂掉电话了。Tidy很苦恼,这么算他真的会崩溃的,聪明的读者,你能写个程序帮他完成这项工作吗?不过如果你的程序效率不够高的话,Tidy还是会受到Derek的责骂的.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值