【异世界情绪】日文翻唱《鳥の詩/鸟之诗》
引个流,情绪的鸟之诗真的好听,听着这个学习效率巨高!
前言
说到线段树,就很难不想到那该死的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风格的吧。写完了,开心~!( '▿ ’ )
然后,快去听情绪的《鳥の詩/鸟之诗》!