没有看别的资料,看了看wikipedia上的线段树模板和简单的介绍就会了。。。
其实就是区间二分存储,每一次修改其中任意点的状态时候更新整棵树的数值就好了
“ 线段树 ( Segment Tree )是一种 二叉搜索树 ,它将一个 区间 划分成一些单元区间,每个单元区间对应线段树中的一个 叶结点 。
对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+b)/2],右子树表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树。叶节点数目为N,即整个线段区间的长度。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数, 时间复杂度 为O(logN)。而未优化的 空间复杂度 为2N,因此有时需要 离散化 让空间压缩。 ”————————————wikipedia从上述描述我们可以看出,在定义线段树的时候数组的长度至少是2倍的区间长度(事实上最好再大一点到4倍,实在开不了太大也要多开上一两万个单元,以防万一)
“ 给定整个线段区间,建立一棵线段树的时间复杂度是O(N) 。单点修改的时间复杂度是O(log N) 。单点查询的时间复杂度是 O(1) 。如果允许惰性赋值而加上延迟标记的话,许多的区间修改的时间复杂度也会是 O(log N) ,但是单点查询的时间复杂度会变成O(log N) 。
常见的线段树主要分为两类,一种是支持区间求和,另一种是求区间最值(当然不仅仅是这两种应用,还可以用线段树保存很多状态)
对于这些问题,我们如果手动模拟去做,很显然一定会TLE的,也得不到很高的分数,这时候就需要线段树优越的时间复杂度了。
关于惰性标记:
惰性标记是标志对从某个起点开始,一定长度区间内的所有数进行的某种操作,在进行区间操作时常常使用惰性标记进行优化来减少时间复杂度。
至于为什么叫惰性,别问我,我不知道。
那么接下来我们来看一下线段树的基本操作(来自wikipedia)。
顺便学会了宏定义的高端使用方法(P.S.wikipedia上的代码里的rchild的宏定义有问题)
首先
宏定义lchild,rchild(可以减少代码量,省力省时)
#define lchild rt << 1, l, m
#define rchild rt << 1|1, m + 1, r
(位运算的定义可以加快速度,左子树是该节点代表区间的左半部分,右子树亦然)
对于那些不了解C++的
<<1就是乘2,>>1就是整除2,|1就是加1(其本质是二进制的左移一位右移一位末尾加一)
建树操作
void build(int rt= 1,int l =1, int r= N)
{
if (l== r)
{
cin >> tree[rt]; //当然也可以用scanf等等
return;
}
int m =(l + r)>> 1;
build(lchild); build(rchild);
push_up(rt); //数据向上更新
}
向上更新整棵树中的节点的数值
/* 对于区间求和 */
void push_up(int rt){ tree[rt]= tree[rt<< 1]+ tree[rt<< 1| 1];}
/* 对于区间求最大值 */
void push_up(int rt){ tree[rt]= max(tree[rt<< 1], tree[rt<< 1| 1]); }
单点操作
void update(int p, int delta,int rt=1,int l=1,int r= N)
{
if
(l == r)
{
tree[rt]+= delta;
return;
}
int m =(l + r)>> 1;
if (p<= m)
update(p, delta, lchild);
else update(p, delta, rchild);
push_up(rt);
}
其中P为目标点的编号,delta是变化值
区间操作
void update(int L,int R, int delta,int rt =1, int l= 1,int r = N)
{
if (L<= l && r<= R)
{
tree[rt]+= delta* (r- l +1);
lazy[rt]+= delta;
return;
}
if (lazy[rt])
push_down(rt, r - l + 1); //更新惰性标记
int m =(l + r)>> 1;
if (L<= m) update(L, R, delta, lchild);
if (R> m) update(L, R, delta, rchild);
push_up(rt);
}
将目标区间分割,然后对两块小区间分别进行操作
更新惰性标记(区间求和)
“ 对于区间求和, 原子数组值需要加上lazy标记乘以子树所统计的区间长度。 len为父节点统计的区间长度, 则len - (len >> 1)为左子树区间长度, len >> 1为右子树区间长度。 ”——————wikipedia
void push_down(int rt,int len)
{
tree[rt << 1]+= lazy[rt]* (len- (len>> 1));
lazy[rt << 1]+= lazy[rt];
tree[rt << 1 | 1]+= lazy[rt]* (len>> 1);
lazy[rt << 1 | 1]+= lazy[rt];
lazy[rt]= 0;
}
更新惰性标记(区间最值)
“ 对于区间求最大值, 子树的值不需要乘以长度, 所以不需要传递参数len。 ” ——————wikipedia
void push_down(int rt)
{
tree[rt << 1]+= lazy[rt];
lazy[rt << 1]+= lazy[rt];
tree[rt << 1 | 1]+= lazy[rt];
lazy[rt << 1 | 1]+= lazy[rt];
lazy[rt]= 0;
}
区间数值查询
int query(int L,int R, int rt= 1,int l =1, int r= N)
{
if (L<= l && r<= R)return tree[rt];
if (lazy[rt]) push_down(rt, r - l +1);
int m =(l + r)>> 1, ret= 0;
if (L<= m) ret+= query(L, R, lchild);
if (R> m) ret+= query(L, R, rchild);
return ret;
}
至于单点数据查询,输出对应的tree数组中元素值就好了
那么让我们看几个最简单地裸线段树的例题(来自Codevs)
1.codevs1080 线段树练习
题目描述
Description
一行N个方格,开始每个格子里都有一个整数。现在动态地提出一些问题和修改:提问的形式是求某一个特定的子区间[a,b]中所有元素的和;修改的规则是指定某一个格子x,加上或者减去一个特定的值A。现在要求你能对每个提问作出正确的回答。1≤N<100000,,提问和修改的总数m<10000条。
输入描述
Input Description
输入文件第一行为一个整数N,接下来是n行n个整数,表示格子中原来的整数。接下一个正整数m,再接下来有m行,表示m个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。
输出描述
Output Description
共m行,每个整数
样例输入
Sample Input
6
4
5
6
2
1
3
4
1 3 5
2 1 4
1 1 9
2 2 6
样例输出
Sample Output
22
22
数据范围及提示
Data Size & Hint
1≤N≤100000, m≤10000 。
单点修改,区间求和。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,m;
int a,b,c;
int tree[400001];
int arr[100001];
void push_up(int rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void build(int rt=1,int l=1,int r=n)
{
if (l==r)
{
tree[rt]=arr[l];
return;
}
int m=(l+r)>>1;
build(lchild);build(rchild);
push_up(rt);
}
void update(int p,int delta,int rt=1,int l=1,int r=n)
{
if (l==r)
{
tree[rt]+=delta;
return;
}
int m=(l+r)>>1;
if (p<=m) update(p,delta,lchild);
else update(p,delta,rchild);
push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R) return tree[rt];
int m=(l+r)>>1, ret=0;
if (L<=m) ret+=query(L,R,lchild);
if (R>m) ret+=query(L,R,rchild);
return ret;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",arr+i);
build(1,1,n);
scanf("%d",&m);
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
switch(a)
{
case 1:
update(b,c);
break;
case 2:
cout<<query(b,c)<<endl;
break;
}
}
system("pause");
}
2.codevs1081线段树练习2
题目描述
Description
给你N个数,有两种操作
1:给区间[a,b]的所有数都增加X
2:询问第i个数是什么?
输入描述
Input Description
第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,表示操作的个数. 接下来Q行每行若干个整数。如果第一个数是1,后接3个正整数a,b,X,表示在区间[a,b]内每个数增加X,如果是2,后面跟1个整数i, 表示询问第i个位置的数是多少。
输出描述
Output Description
对于每个询问输出一行一个答案
样例输入
Sample Input
3
1
2
3
2
1 2 3 2
2 3
样例输出
Sample Output
5
数据范围及提示
Data Size & Hint
数据范围
1<=n<=100000
1<=q<=100000
区间修改,单点查询。(将单点当成左右端点相同的区间即可)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,q,tree[210000],lazy[210000];
int a,b,c,d;
void push_up(int rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void push_down(int rt, int len)
{
tree[rt<<1]+=lazy[rt]*(len-(len>>1));
lazy[rt<<1]+=lazy[rt];
tree[rt<<1|1]+=lazy[rt]*(len>>1);
lazy[rt<<1|1]+=lazy[rt];
lazy[rt]=0;
}
void build(int rt=1,int l=1,int r=n)
{
if (l==r)
{
scanf("%d",&tree[rt]);
return;
}
int m=(l+r)>>1;
build(lchild);
build(rchild);
push_up(rt);
}
void update(int L,int R,int delta,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R)
{
tree[rt]+=delta*(r-l+1);
lazy[rt]+=delta;
return;
}
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1;
if (L<=m) update(L,R,delta,lchild);
if (R>m) update(L,R,delta,rchild);
push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R) return tree[rt];
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1,ret=0;
if (L<=m) ret+=query(L,R,lchild);
if (R>m) ret+=query(L,R,rchild);
return ret;
}
int main()
{
scanf("%d",&n);
build();
scanf("%d",&q);
for (int i=1;i<=q;i++)
{
scanf("%d",&a);
switch (a)
{
case 1:
scanf("%d%d%d",&b,&c,&d);
update(b,c,d);
break;
case 2:
scanf("%d",&b);
cout<<query(b,b)<<endl;
break;
}
}
}
3.codevs1082线段树练习3
题目描述
Description
一行N个方格,开始每个格子里都有一个整数。现在动态地提出一些问题和修改:提问的形式是求某一个特定的子区间[a,b]中所有元素的和;修改的规则是指定某一个格子x,加上或者减去一个特定的值A。现在要求你能对每个提问作出正确的回答。1≤N<100000,,提问和修改的总数m<10000条。
输入文件第一行为一个整数N,接下来是n行n个整数,表示格子中原来的整数。接下一个正整数m,再接下来有m行,表示m个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。
共m行,每个整数
6
4
5
6
2
1
3
4
1 3 5
2 1 4
1 1 9
2 2 6
22
22
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,m;
int a,b,c;
int tree[400001];
int arr[100001];
void push_up(int rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void build(int rt=1,int l=1,int r=n)
{
if (l==r)
{
tree[rt]=arr[l];
return;
}
int m=(l+r)>>1;
build(lchild);build(rchild);
push_up(rt);
}
void update(int p,int delta,int rt=1,int l=1,int r=n)
{
if (l==r)
{
tree[rt]+=delta;
return;
}
int m=(l+r)>>1;
if (p<=m) update(p,delta,lchild);
else update(p,delta,rchild);
push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R) return tree[rt];
int m=(l+r)>>1, ret=0;
if (L<=m) ret+=query(L,R,lchild);
if (R>m) ret+=query(L,R,rchild);
return ret;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",arr+i);
build(1,1,n);
scanf("%d",&m);
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&a,&b,&c);
switch(a)
{
case 1:
update(b,c);
break;
case 2:
cout<<query(b,c)<<endl;
break;
}
}
system("pause");
}
给你N个数,有两种操作
1:给区间[a,b]的所有数都增加X
2:询问第i个数是什么?
第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,表示操作的个数. 接下来Q行每行若干个整数。如果第一个数是1,后接3个正整数a,b,X,表示在区间[a,b]内每个数增加X,如果是2,后面跟1个整数i, 表示询问第i个位置的数是多少。
对于每个询问输出一行一个答案
3
1
2
3
2
1 2 3 2
2 3
5
数据范围
1<=n<=100000
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
int n,q,tree[210000],lazy[210000];
int a,b,c,d;
void push_up(int rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void push_down(int rt, int len)
{
tree[rt<<1]+=lazy[rt]*(len-(len>>1));
lazy[rt<<1]+=lazy[rt];
tree[rt<<1|1]+=lazy[rt]*(len>>1);
lazy[rt<<1|1]+=lazy[rt];
lazy[rt]=0;
}
void build(int rt=1,int l=1,int r=n)
{
if (l==r)
{
scanf("%d",&tree[rt]);
return;
}
int m=(l+r)>>1;
build(lchild);
build(rchild);
push_up(rt);
}
void update(int L,int R,int delta,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R)
{
tree[rt]+=delta*(r-l+1);
lazy[rt]+=delta;
return;
}
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1;
if (L<=m) update(L,R,delta,lchild);
if (R>m) update(L,R,delta,rchild);
push_up(rt);
}
int query(int L,int R,int rt=1,int l=1,int r=n)
{
if (L<=l&&r<=R) return tree[rt];
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1,ret=0;
if (L<=m) ret+=query(L,R,lchild);
if (R>m) ret+=query(L,R,rchild);
return ret;
}
int main()
{
scanf("%d",&n);
build();
scanf("%d",&q);
for (int i=1;i<=q;i++)
{
scanf("%d",&a);
switch (a)
{
case 1:
scanf("%d%d%d",&b,&c,&d);
update(b,c,d);
break;
case 2:
scanf("%d",&b);
cout<<query(b,b)<<endl;
break;
}
}
}
给你N个数,有两种操作:
1:给区间[a,b]的所有数增加X
2:询问区间[a,b]的数的和。
第一行一个正整数n,接下来n行n个整数,
再接下来一个正整数Q,每行表示操作的个数,
如果第一个数是1,后接3个正整数,
表示在区间[a,b]内每个数增加X,如果是2,
表示操作2询问区间[a,b]的和是多少。
对于每个询问输出一行一个答案
3
1
2
3
2
1 2 3 2
2 2 3
9
数据范围
1<=n<=200000
1<=q<=200000
区间操作,区间查询(其实本来和第二题一样,但是数据范围巨大,将所有函数都改为longlong之后就没事了)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define lchild rt<<1,l,m
#define rchild rt<<1|1,m+1,r
using namespace std;
long long n,q,tree[810000],lazy[810000];
long long a,b,c,d;
void push_up(long long rt)
{
tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}
void push_down(long long rt, long long len)
{
tree[rt<<1]+=lazy[rt]*(len-(len>>1));
lazy[rt<<1]+=lazy[rt];
tree[rt<<1|1]+=lazy[rt]*(len>>1);
lazy[rt<<1|1]+=lazy[rt];
lazy[rt]=0;
}
void build(long long rt=1,long long l=1,long long r=n)
{
if (l==r)
{
scanf("%lld",&tree[rt]);
return;
}
int m=(l+r)>>1;
build(lchild);
build(rchild);
push_up(rt);
}
void update(long long L,long long R,long long delta,long long rt=1,long long l=1,long long r=n)
{
if (L<=l&&r<=R)
{
tree[rt]+=delta*(r-l+1);
lazy[rt]+=delta;
return;
}
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1;
if (L<=m) update(L,R,delta,lchild);
if (R>m) update(L,R,delta,rchild);
push_up(rt);
}
long long query(long long L,long long R,long long rt=1,long long l=1,long long r=n)
{
if (L<=l&&r<=R) return tree[rt];
if (lazy[rt]) push_down(rt,r-l+1);
int m=(l+r)>>1;
long long ret=0;
if (L<=m) ret+=query(L,R,lchild);
if (R>m) ret+=query(L,R,rchild);
return ret;
}
int main()
{
scanf("%lld",&n);
build();
scanf("%lld",&q);
for (int i=1;i<=q;i++)
{
scanf("%lld",&a);
switch (a)
{
case 1:
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,d);
break;
case 2:
scanf("%lld%lld",&b,&c);
printf("%lld\n",query(b,c));
break;
}
}
}
小结:作为一种中级数据结构,线段树在很多方面都有应用,在OI竞赛中,大部分时候线段树并不是单独出现也不止区间求和和区间最值两种,而是综合了多种问题或者进行了改进和加强,如 Codevs1217 借教室 (2012年NOIP全国联赛提高组Day2T2)Codevs19461946 阿狸的打字机(2011年NOI全国竞赛)等等
为了解决这些题目,首先就需要线段树的熟练掌握。 今天的题目只是一些裸线段树的基础中的基础,巩固线段树这种数据结构还需要在今后的练习中反复强化