1.写在前面
对线段树这个东西窝也是刚刚才算搞明白,对于很多东西还不是很清楚,只讲一些很简单的东西分享一下,也是等于做一个记录,这样到时候我自己忘掉了也还能看看这个博客想起来……代码全部是C++的,总之一句话:错了别怪我~
- ┭┮﹏┭┮
2.线段树是什么
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。——From 度娘
反正就是一种可以在很短的时间内对某个区间进行操作的数据结构。
3.线段树有什么用
在O(logN)的时间复杂度内实现如:单点修改、区间修改、区间查询(如:区间求和,求区间最大值,求区间最小值……)还有很多……
4.线段树怎么实现
1.线段树的基本结构与建树
- 想要建立一棵线段树,不理解它的结构、原理是肯定行不通的。
下面我来举个栗子:
我们有个大小为5的数组a{10,11,12,13,14},要进行区间求和操作,现在我们要怎么把这个数组存到线段树中(也可以说是转化成线段树)呢?我们这样子做:设线段树的根节点编号为1,用数组d来保存我们的线段树,d[i]用来保存编号为i的节点的值(这里节点的值就是这个节点所表示的区间总和),如图所示:
图中d[1]表示根节点,紫色方框是数组a,红色方框是数组d,红色方框中的括号中的黄色数字表示它所在的那个红色方框表示的线段树节点所表示的区间,如d[1]所表示的区间就是1~5(a[1]~a[5]),即d[1]所保存的值是a[1]+a[2]+…+a[5],d[1]=60表示的是a[1]+a[2]+…+a[5]=60。
通过观察我们不难发现,d[i]的左儿子节点就是d[i*2],d[i]的右节点就是d[i*2+1]。进一步观察,可以看出如果d[i]表示的是区间s~t(即d[i]=a[s]+a[s+1]+…+a[t])的话,那么d[i]的左儿子节点表示的是区间s~((s+t)/2),d[i]的右儿子表示的是区间((s+t)/2+1)~t。为什么要这样表示呢?因为线段树利用了二分的思想,线段树实际上是个二叉树,这些不懂的话就无法理解线段树了,所以如果不明白二分或者二叉树的话……建议去问问度娘。
具体要怎么用代码实现呢?我们继续观察,有没有发现如果d[i]表示的区间大小==1(区间大小指的是区间包含的元素的个数(即a的个数))的话(设d[i]表示区间s~t,它的区间大小就是(t-s+1),不信你看上面的图),那么d[i]所表示的区间s~t中s肯定==t(不信你还是看图),且d[i]=a[s](当然也=a[t])
为什么要讲这个东西呢?你没发现这个是个递归边界吗?
O(∩_∩)O哈哈~
–
思路如下:
那么就这样写代码:
建树(s,t,i)
{
如果(s==t),则d[i]=a[s];
否则 建树(s,(s+t)/2,i*2),建树((s+t)/2+1,t,i*2+1);
d[i]=d[i*2]+d[i*2+1];
}
具体代码实现(c++):
void build(int s,int t,int i)
{
if(s==t){d[i]=a[s];return;}
int m=(s+t)/2;
build(s,m,i*2),build(m+1,t,i/2+1);
d[p]=d[p*2]+d[(p*2)+1];
}
上面那短短7行代码就能建立一个线段树。
其实还有一个比较严重的问题:数组d到底开多大?如果a数组中元素个数是n,那d数组的元素个数应该定为多少?保险起见,为了防止你的d数组越界、程序爆炸的话,d数组大小应该为n*4,再保险一点的话定为n*4+5吧。为啥是n*4呢?这里我转载一下一篇博客,里面有详细的讲解,我就不讲了(我懒行了吧)……
传送门:http://scinart.github.io/acm/2014/03/19/acm-segment-tree-space-analysis/
2.线段树的区间查询
- 区间查询,比如求区间[l,r]的总和(即a[l]+a[l+1]+…+a[r])、求区间最大值/最小值……还有很多很多……怎么做呢?
拿上面这张图
举栗!
(发博客累死了无聊一下)
如果要查询区间[1,5]的和,那直接获取d[1]的值(60)即可。那如果我就不查询区间[1,5],我就查区间[3,5]呢?
Σ(⊙▽⊙”a
懵B了吧。但其实呢我们肯定还是有办法的!
<( ̄ˇ ̄)/
你要差的不是[3,5]吗?我把[3,5]拆成[3,3]和[4,5]不就行了吗?
具体思路见代码:
求和(查询区间的左端点l,查询区间的右端点r,当前节点表示的区间左端点s,当前节点表示的区间t,当前访问的节点编号p)
{
如果(l<=s&&t<=r)//当前访问的节点表示的区间包含在查询区间内
返回d[p];
否则
{
设 返回值=0如果(l<=(s+t)/2)//当前访问的节点的左儿子节点表示的区间包含在查 询区间内,(s+t)/2其实是左右儿子节点表示的区间的分割线且(s+t)/2包含在左儿子节点表示的区间中
{
返回值+=求和(l,r,s,(s+t)/2,p*2);//l和r是可以不用变的!不管你信不信我反正是信了。当前节点的左儿子节点编号是p*2,之前讲过了,左儿子节点表示的区间左端点就是当前节点表示的区间的左端点,(s+t)/2是左儿子节点表示的区间的右短点
}
如果(r>(s+t)/2)//当前访问的节点的右儿子节点表示的区间包含在查 询区间内
{
返回值+=求和(l,r,(s+t)/2+1,t,p*2+1);//(s+t)/2+1是当前访问节点的右儿子节点表示的区间的左端点
}
返回 返回值;
}
}
怎么样,代码很丑吧?废话,用中文写的能不丑吗?现在搞个英(da)文(xin)的(wen):
int getsum(int l,int r,int s,int t,int p)
{
if(l<=s&&t<=r)return d[p];
int m=(s+t)/2,sum=0;
if(l<=m)sum+=getsum(l,r,s,m,p/2);
if(r>m)sum+=getsum(l,r,m+1,t,p/2+1);
return sum;
}
还是挺短的吧?这里用到的主要思路就是把一个区间拆成左右两个区间,再分别处理左右区间。也是二分的思想。
3.线段树的区间修改与懒惰标记
- 区间修改是个很有趣的东西o(╯□╰)o……你想啊,如果你要修改区间[l,r],难道把所有包含在区间[l,r]中的节点都遍历一次、修改一次?那估计这时间复杂度估计会上天|(*′口`)。这怎么办呢?我们这里要引用一个叫做
“懒惰标记”
- 的
“恶心”
东西……
我们设一个数组b,b[i]表示编号为i的节点的懒惰标记值。啥是懒惰标记、懒惰标记值呢?(O_O)?这里我再举个栗子(原创小故事我真有才哈哈哈(◡ᴗ◡✿)):- A有两个儿子,一个是B,一个是C。有一天A要建一个新房子,没钱。刚好过年嘛,有人要给B和C红包,两个红包的钱数相同都是(1000000000000001%2)圆(好多啊!……不就是1元吗O(∩_∩)O~……),然而因为A是父亲所以红包肯定是先塞给A咯~理论上来讲A应该把两个红包分别给B和C,但是……缺钱嘛,A就把红包偷偷收到自己口袋里了。A高兴♂地说:“我现在有2份红包了!我又多了2*(1000000000000001%2)=2圆了!哈哈哈~”但是A知道,如果他不把红包给B和C,那B和C肯定会不爽然后导致家庭矛盾最后崩溃,所以A对儿子B和C说:“我欠你们每人1份(1000000000000001%2)圆的红包,下次有新红包给过来的时候再给你们!这里我先做下记录……嗯……我钱你们各(1000000000000001%2)圆……”。儿子B、C有点恼怒:“可是如果有同学问起我们我们收到了多少红包咋办?你把我们的红包都收了,我们还怎么装×?”父亲A赶忙说:“有同学问起来我就会给你们的!我欠条都写好了不会不算话的!”这样B、C才放了心。(注:%是取余数的意思,a%b就是a除以b的余数,所以……1000000000000001%2=1)
在这个故事中我们不难看出,A就是父亲节点,B和C是A的儿子节点,而且B和C是叶子节点,分别对应一个数组中的值(就是之前讲的数组a),我们假设节点A表示区间[1,2](即a[1]+a[2]),节点B表示区间[1,1](即a[1]),节点C表示区间[2,2](即a[2]),它们的初始值都为0(现在才刚开始呢,还没拿到红包,所以都没钱~)。如图:
注:这里D表示当前节点的值(即所表示区间的区间和)
为什么节点A的D是2*(1000000000000001%2)呢?原因很简单。节点A表示的区间是[1,2],一共包含2个元素。我们是让[1,2]这个区间的每个元素都加上1000000000000001%2,所以节点A的值就加上了2*(1000000000000001%2)咯 = ̄ω ̄= 。
- 如果这时候我们要查询区间[1,1](即节点B的值)怎么办呢?不是说了吗?如果B要用到的时候,A就把它欠的还给B!具体是这样操作(如图):
注:为什么是加上1*(1000000000000001%2)呢?原因和上面一样——B和C表示的区间中只有1个元素啊!
由此我们可以得到,区间[1,1]的区间和就是1啦!O(∩_∩)O哈哈~!
代码如下(下面代码不知道为什么显示出来很丑,建议复制到自己的C++编辑器里看……/(ㄒoㄒ)/~~):
区间修改(区间加上某个值):
void update(int l,int r,int c,int s,int t,int p)//l是查询的区间左端点,r是右端点,c表示区间每个元素加上的值,s是当前节点所表示的区间的左端点,t是右端点,p是当前节点的编号(根节点标号为1)
{
if(l<=s&&t<=r){d[p]+=(t-s+1)*c,b[p]+=c;return;}//如果当前节点表示的区间完全包含在查询区间内,直接修改当前节点的值,然后做上标记,结束修改
int m=(s+t)/2;//计算左右节点表示区间的分割线
if(b[p]&&s!=t)//如果当前节点不是叶子节点(叶子节点表示的区间的左右端点是相等的)且当前的懒惰标记值!=0,就更新当前节点的两个儿子节点的值和懒惰标记值
d[p*2]+=b[p]*(m-s+1),d[p*2+1]+=b[p]*(t-m),b[p*2]+=b[p],b[p*2+1]+=b[p];
b[p]=0;//清空当前节点的懒惰标记值
if(l<=m)update(l,r,c,s,m,p*2);
if(r>m)update(l,r,c,m+1,t,p*2+1);
d[p]=d[p*2]+d[p*2+1];
}
区间查询(求和):
int getsum(int l,int r,int s,int t,int p)//l是查询的区间左端点,r是右端点,s是当前节点所表示的区间的左端点,t是右端点,p是当前节点的编号(根节点标号为1)
{
if(l<=s&&t<=r)return d[p];//如果当前节点表示的区间完全包含在查询区间内,返回当前节点的值
int m=(s+t)/2;//计算左右节点表示区间的分割线
if(b[p]&&s!=t)//如果当前节点不是叶子节点(叶子节点表示的区间的左右端点是相等的)且当前的懒惰标记值!=0,就更新当前节点的两个儿子节点的值和懒惰标记
d[p*2]+=b[p]*(m-s+1),d[p*2+1]+=b[p]*(t-m),b[p*2]+=b[p],b[p*2+1]+=b[p];
b[p]=0;int sum=0;//清空当前节点的懒惰标记值
if(l<=m)sum=getsum(l,r,s,m,p*2);
if(r>m)sum+=getsum(l,r,m+1,t,p*2+1);
return sum;
}
你有没有发现区间查询和区间修改很像吗?(…^__^…) 嘻嘻……其实平时我打线段树区间修改和查询我都是打一份,另一份复制黏贴以后再稍作修改就行了。
如果你是要实现区间修改为某一个值而不是加上某一个值的话,很简单,把上面的代码中所有的+=替换成=即可(除了sum+=getsum(l,r,m+1,t,p*2+1)这一句)。代码如下:
void update(int l,int r,int c,int s,int t,int p)
{
if(l<=s&&t<=r){d[p]=(t-s+1)*c,b[p]=c;return;}
int m=(s+t)/2;
if(b[p]&&s!=t)
d[p*2]=b[p]*(m-s+1),d[p*2+1]=b[p]*(t-m),b[p*2]=b[p*2+1]=b[p];
b[p]=0;
if(l<=m)update(l,r,c,s,m,p*2);
if(r>m)update(l,r,c,m+1,t,p*2+1);
d[p]=d[p*2]+d[p*2+1];
}
int getsum(int l,int r,int s,int t,int p)
{
if(l<=s&&t<=r)return d[p];
int m=(s+t)/2;
if(b[p]&&s!=t)
d[p*2]=b[p]*(m-s+1),d[p*2+1]=b[p]*(t-m),b[p*2]=b[p*2+1]=b[p];
b[p]=0;int sum=0;
if(l<=m)sum=getsum(l,r,s,m,p*2);
if(r>m)sum+=getsum(l,r,m+1,t,p*2+1);
return sum;
}
- 以上就是线段树的实现方法,由于是分几天写的,所以有点凌乱,可能出错误,欢迎大家指正~OrzOrzOrz
5.线段树基础题推荐
- 这里呢……我自己也没做多少题,所以只有几道题有题解,而且……本人蒟蒻一枚,代码丑……
- 1.LUOGU P3372 【模板】线段树 1
传送门:https://www.luogu.org/problem/show?pid=3372
题解(c++):
- 1.LUOGU P3372 【模板】线段树 1
#include <iostream>
using namespace std;
long long n,a[100005],d[270000],b[270000];
void build(long long l,long long r,long long p)
{
if(l==r){d[p]=a[l];return;}
long long m=(l+r)>>1;
build(l,m,p<<1),build(m+1,r,(p<<1)|1);
d[p]=d[p<<1]+d[(p<<1)|1];
}
void update(long long l,long long r,long long c,long long s,long long t,long long p)
{
if(l<=s&&t<=r){d[p]+=(t-s+1)*c,b[p]+=c;return;}
long long m=(s+t)>>1;
if(b[p]&&s!=t)
d[p<<1]+=b[p]*(m-s+1),d[(p<<1)|1]+=b[p]*(t-m),b[p<<1]+=b[p],b[(p<<1)|1]+=b[p];
b[p]=0;
if(l<=m)update(l,r,c,s,m,p<<1);
if(r>m)update(l,r,c,m+1,t,(p<<1)|1);
d[p]=d[p<<1]+d[(p<<1)|1];
}
long long getsum(long long l,long long r,long long s,long long t,long long p)
{
if(l<=s&&t<=r)return d[p];
long long m=(s+t)>>1;
if(b[p]&&s!=t)
d[p<<1]+=b[p]*(m-s+1),d[(p<<1)|1]+=b[p]*(t-m),b[p<<1]+=b[p],b[(p<<1)|1]+=b[p];
b[p]=0;long long sum=0;
if(l<=m)sum=getsum(l,r,s,m,p<<1);
if(r>m)sum+=getsum(l,r,m+1,t,(p<<1)|1);
return sum;
}
int main()
{
ios::sync_with_stdio(0);
long long q,i1,i2,i3,i4;
cin>>n>>q;
for(long long i=1;i<=n;i++)cin>>a[i];
build(1,n,1);
while(q--)
{
cin>>i1>>i2>>i3;
if(i1==2)cout<<getsum(i2,i3,1,n,1)<<endl;
else cin>>i4,update(i2,i3,i4,1,n,1);
}
return 0;
}
-
2.LUOGU P3373 【模板】线段树 2
传送门:https://www.luogu.org/problem/show?pid=3372
题解:无(…^__^…) 嘻嘻……3.CODEVS 线段树练习 (这是一个系列)
传送门:
http://codevs.cn/problem/?q=%E7%BA%BF%E6%AE%B5%E6%A0%91%E7%BB%83%E4%B9%A0
题解:无~(其实我还是做了练习1、2、3,但是其中的1、2我是用树状数组做的,3我懒得发了……)4.HihoCoder 1078 线段树的区间修改
传送门(vjudge):https://cn.vjudge.net/problem/HihoCoder-1078
题解:
#include <iostream>
using namespace std;
int n,a[100005],d[270000],b[270000];
void build(int l,int r,int p)
{
if(l==r){d[p]=a[l];return;}
int m=(l+r)>>1;
build(l,m,p<<1),build(m+1,r,(p<<1)|1);
d[p]=d[p<<1]+d[(p<<1)|1];
}
void update(int l,int r,int c,int s,int t,int p)
{
if(l<=s&&t<=r){d[p]=(t-s+1)*c,b[p]=c;return;}
int m=(s+t)>>1;
if(b[p]&&s!=t)
d[p<<1]=b[p]*(m-s+1),d[(p<<1)|1]=b[p]*(t-m),b[p<<1]=b[(p<<1)|1]=b[p];
b[p]=0;
if(l<=m)update(l,r,c,s,m,p<<1);
if(r>m)update(l,r,c,m+1,t,(p<<1)|1);
d[p]=d[p<<1]+d[(p<<1)|1];
}
int getsum(int l,int r,int s,int t,int p)
{
if(l<=s&&t<=r)return d[p];
int m=(s+t)>>1;
if(b[p]&&s!=t)
d[p<<1]=b[p]*(m-s+1),d[(p<<1)|1]=b[p]*(t-m),b[p<<1]=b[(p<<1)|1]=b[p];
b[p]=0;int sum=0;
if(l<=m)sum=getsum(l,r,s,m,p<<1);
if(r>m)sum+=getsum(l,r,m+1,t,(p<<1)|1);
return sum;
}
int main()
{
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
build(1,n,1);
int q,i1,i2,i3,i4;
cin>>q;
while(q--)
{
cin>>i1>>i2>>i3;
if(i1==0)cout<<getsum(i2,i3,1,n,1)<<endl;
else cin>>i4,update(i2,i3,i4,1,n,1);
}
return 0;
}
好了,就写这么多了。以上是线段树入门的所有内容,都很基础,希望大家都能学会~OrzOrzOrz