本文是个人通过部分博文学习线段树的笔记,并且写一下自己的理解。文首标明参考文章出处。本文部分内容借鉴自于:
目录
2. Can you answer these queries? HDU - 4027
一. 背景概述
听名字,线段树,是来处理一段一段的区间的。我们来看一个例题:
题意
给出n个数字序列,给出m个操作,每个操作输入a , b, c
- 若a == 1 :求出 [ b, c ] 区间内数字的和是多少
- 若a == 2:修改b处数字+c
这个题应该怎么来解决?普通的我们求和可能先预处理一下前缀和,但是修改某一点的话,我们所有的前缀和都要修改一遍,这里求和快修改慢;如果每次是一个个求和,求和太慢但是修改只修改一个点就行了,这里求和慢修改快。
那有什么办法求和也快修改也快呢?这里就有了线段树 :把前缀和建在树上,前缀的是子节点的和,修改时只修改与修改点有联系的父节点,这就是线段树在该题的思路,也是线段树产生的背景。
二. 实现思路
1. 关系划分
假设我们区间是 [ 1 , 13 ] , 首先我们来划分区间父子节点关系如下图:
.
怎么划分?即自上而下二分:给定区间[L,R],只要L < R ,线段树就会把它继续分裂成两个区间。
首先计算 M = (L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就递归分解。
2. 区间统计
接下来如何来进行区间统计呢?这个以数组 [ 1 , 13 ] = { 1 , 2 , 3 , 4 , 1 , 2 , 4 , 1 , 3 , 4 } 为例,,其父节点前缀和如图:
每一个父节点都是其子节点的和,这样递归合并下去。如果我们要求[ 4 , 11 ] 的和:首先自上而下:sum = 0
(1)二分 [ 1 , 13 ] 为 [ 1,7] + [8 , 13] :4<=7&&11>7说明要求的区间有两部分分别在 [ 1,7 ]和 [ 8 , 13 ]里面,继续递归
(2) 二分 [ 1 , 7] 为 [ 1 , 4 ] + [5 , 7 ]:在 [ 1,7 ]里面的那部分又分为了 [ 1 , 4 ] 和 [5 , 7 ]两部分,而4<=4&&11>4,继续递归
(3)二分 [ 8 , 13]为 [ 8 ,10 ] + [11 , 13] :在 [ 8,13 ]里面的那部分又分为了 [ 8 , 10 ] 和 [11 , 13 ]两部分,而4<=10&&11>10,继续递归
(4)二分 [ 1 , 4] 为 [ 1 , 2 ] + [3 , 4 ]:在 [ 1,4 ]里面的那部分又分为了 [ 1 , 2 ] 和 [3 , 4 ]两部分,而11>2但4也>2,只递归右子树,说明[1,2]并不在[ 4 ,11 ]里面
(5)现在递归到[5 , 7 ] ,发现[5 , 7 ]全部都在 [ 4, 11 ]里面 ,sum+=ans[ 5 ,7 ] ;return;
(6)...................
这样一直递归下去,最后求出的就是要查询的区间和。结束Over。
3. 点修改
接下来如何进行点修改呢?假设我们这里要修改的点是 6 这个位置:
我们发现要修改的地方只是该节点的父节点即可,仍是递归实现即可。
三. 实现代码
思考对于每个区间节点我们怎么表示呢?可以采用传统的 n 的左子节点是 n<<1 , 右子节点是 n<<1|1来用数组保存。所以这里保存节点的数组大小最好开到长度的四倍。
const int maxn = 50000 + 10;
int num[maxn];
int sum[maxn<<2];//保存区间节点
int n;
1. 建树
void BuildTree(int l,int r,int root)//当前区间左边界l , 右边界 r , 当前节点
{
if(l==r){//递归到叶子赋值
sum[root] = man[l];
return;
}
int m = (l+r)>>1;
BuildTree(l,m,root<<1);//递归左右子树
BuildTree(m+1,r,root<<1|1);
sum[root] = sum[root<<1] + sum[root<<1|1];//更新该节点
}
2. 查询区间
int Query(int L,int R,int l,int r,int root)//要查询的区间 [ L,R ],当前区间[l,r],当前节点
{
if(L<=l&&R>=r){//如果完全包含了这个区间,直接返回
return sum[root];
}
int m = (l+r)>>1;
int ANS = 0;//记录左右子树和
if(L<=m)ANS+=Query(L,R,l,m,root<<1);//判断能否继续递归
if(R>m)ANS+=Query(L,R,m+1,r,root<<1|1);
return ANS;
}
3. 点修改
void Update(int p,int c,int l,int r,int root)//要修改的点和+c ,当前区间 [l,r],当前节点
{
if(l==r){//递归到叶子就是要修改的子位置
sum[root]+=c;
return;
}
int m = (l+r)>>1;
if(p<=m)Update(p,c,l,m,root<<1);//判断要修改点的位置
else if(p>m)Update(p,c,m+1,r,root<<1|1);
sum[root] = sum[root<<1]+sum[root<<1|1];//更新节点
}
4. 总板子(HDU - 1166 排兵布阵)
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn = 50000 + 10;
int num[maxn];
int sum[maxn<<2],n;
void BuildTree(int l,int r,int root)
{
if(l==r){
sum[root] = man[l];
return;
}
int m = (l+r)>>1;
BuildTree(l,m,root<<1);
BuildTree(m+1,r,root<<1|1);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
int Query(int L,int R,int l,int r,int root)
{
if(L<=l&&R>=r){
return sum[root];
}
int m = (l+r)>>1;
int ANS = 0;
if(L<=m)ANS+=Query(L,R,l,m,root<<1);
if(R>m)ANS+=Query(L,R,m+1,r,root<<1|1);
return ANS;
}
void Update(int p,int c,int l,int r,int root)
{
if(l==r){
sum[root]+=c;
return;
}
int m = (l+r)>>1;
if(p<=m)Update(p,c,l,m,root<<1);
else if(p>m)Update(p,c,m+1,r,root<<1|1);
sum[root] = sum[root<<1]+sum[root<<1|1];
}
int main()
{
int T;
int t = 0;
scanf("%d",&T);
while(T--){
t++;
scanf("%d",&n);
for(int i = 1;i<=n;i++){
scanf("%d",&man[i]);
}
BuildTree(1,n,1);
string op;
printf("Case %d:\n",t);
while(cin>>op&&op!="End"){
int a,b;
if(op=="Query"){
scanf("%d%d",&a,&b);
int ans = Query(a,b,1,n,1);
printf("%d\n",ans);
}
else if(op=="Add"){
scanf("%d%d",&a,&b);
man[a]+=b;
Update(a,b,1,n,1);
}
else if(op=="Sub"){
scanf("%d%d",&a,&b);
man[a]-=b;
Update(a,-b,1,n,1);
}
}
}
return 0;
}
四. 例题分析
1. HDU - 1754 I Hate It
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int maxn = 200000 + 10;
int stu[maxn];
int great[maxn<<2],n,m;//求区间最值
void BuildTree(int l,int r,int root)
{
if(l==r){
great[root] = stu[l];
return;
}
int m = (l+r)/2;
BuildTree(l,m,root<<1);
BuildTree(m+1,r,root<<1|1);
great[root] = max(great[root<<1],great[root<<1|1]);
}
int Query(int L,int R,int l,int r,int root)
{
if(L<=l&&R>=r){
return great[root];
}
int m = (l+r)/2;
int lm = -1,rm = -1;
if(L<=m)lm = Query(L,R,l,m,root<<1);
if(R>m)rm = Query(L,R,m+1,r,root<<1|1);
return max(lm,rm);
}
void Update(int p,int c,int l,int r,int root)
{
if(l==r){
great[root] = c;
return;
}
int m = (l+r)/2;
if(p<=m)Update(p,c,l,m,root<<1);
else if(p>m)Update(p,c,m+1,r,root<<1|1);
great[root] = max(great[root<<1],great[root<<1|1]);
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF){
for(int i = 1;i<=n;i++){
scanf("%d",&stu[i]);
}
BuildTree(1,n,1);
for(int i = 1;i<=m;i++){
char op;
int a,b;
getchar();
scanf("%c%d%d",&op,&a,&b);
if(op=='Q'){
int ans = Query(a,b,1,n,1);
printf("%d\n",ans);
}
else if(op=='U'){
stu[a] = b;
Update(a,b,1,n,1);
}
}
}
return 0;
}
2. HDU - 2795 Billboard
(1)题意:给你一块公告板的长 h ,宽 w。给你 n 块公告依次输入他们的宽度,高度都为单位高度,输入顺序代表张贴时间,现在让你输出每块公告张贴的高度,要求张贴位置尽量靠上靠左。(1 <= h,w <= 10^9; 1 <= n <= 200,000)
(2)分析:给你一块公告,我们肯定先从上往下找某个高度的宽度能>=他的地方来张贴这个公告 。 所以我们可以用sum来保存每个区间的当前的最大宽度。如果sum[1](树根)>=公告宽度就说明树里面肯定有能放下这个公告的位置,如果<公告宽度则一定没有。那怎么表示从上到下呢?我们知道小区间是在左子树,所以我们递归的时候优先判断左子树是否可用即可。然后找到叶子修改。
(3)注意:本题目一大坑点,h的范围使用不到的,因为n最大是200000,每个公告是单位长度,就算我每个高度贴一个,最多也就用200000,所以h之取到n范围,大于的取n即可;
(4)代码实现:
#include <iostream>
#include<cstring>
#include<cstdio>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 200000 + 5;
int sum[maxn<<2];
int h,w,n;
void CreatTree(int l,int r,int root)
{
if(l==r){
sum[root] = w;//初始化宽度都为w
return;
}
int m = (l+r)/2;
CreatTree(l,m,root<<1);
CreatTree(m+1,r,root<<1|1);
sum[root] = max(sum[root<<1],sum[root<<1|1]);
}
void Update(int l,int r,int root,int k)
{
if(l==r){
sum[root]-=k;//更新同时打印高度
printf("%d\n",l);
return;
}
int m = (l+r)/2;
if(k<=sum[root<<1]){//优先判断左子树
Update(l,m,root<<1,k);
}
else Update(m+1,r,root<<1|1,k);
sum[root] = max(sum[root<<1],sum[root<<1|1]);
}
int main()
{
while(scanf("%d%d%d",&h,&w,&n)!=EOF){
if(h>n)h = n;//坑点
CreatTree(1,h,1);
for(int i = 1;i<=n;i++){
int len;
scanf("%d",&len);
if(len>sum[1]){
printf("-1\n");
}
else Update(1,h,1,len);
}
}
return 0;
}
五. 线段树的区间修改
1. 问题背景
在上述问题的基础上,如果我要是给你把区间 [ L , R] 的数字都加上 c,你会怎么去修改?
- 做法一:枚举区间,进行R - L + 1次点修改
- 做法二:暴力递归,在修改时递归到每一个子节点进行修改(这里只给出做法二的代码):
void Update(int L,int R,int l,int r,int root,int k)
{
if(l==r){
sum[root] += k;
return;
}
int m = (l+r)/2 ;
if(L<=m)Update(L,R,l,m,root<<1,k);
if(R>m)Update(L,R,m+1,r,root<<1|1,k);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
上面做法是可行的,但是花费时间太长了,修改所有子节点和他们的父节点。那有什么办法加快速度呢?我们发现:
如果我们要让[ 8, 13 ]里面的都加上c ,那我们直接让 [ 8, 13 ] 这个节点加上 (13 - 8 + 1)*c ,不就得了嘛!
但是如果我们下面还要修改查询呢?子节点还是要修改的!但是我们万一不查询呢。所以这里利用这个特点,引入一个"延时标记"表示这个区间是要修改的,但是子节点还没修改。我们在查询或者修改时遇到这个标记就先更新他的子节点。但是我们在最初修改时只递归到这一层就可以返回了:
- [ 8, 13 ] 这个节点加上 (13 - 8 + 1)*c 保证上层结果正确;
- [ 8, 13 ] 这个节点标记上延时标记,保证下层可被修改结果正确;
加快速度保证结果两全其美何乐而不为!
2. 实现代码
void PushDown(int l,int r,int root)
{
if(Change[root]){//如果被标记说明该节点下的子节点还没有更新
//下推标记
Change[root<<1] += Change[root];//子节点标记更新+=
Change[root<<1|1] += Change[root];
//更新子节点
int m = (l+r)/2;
sum[root<<1] += (m - l + 1)*Change[root];
sum[root<<1|1] += (r - m)*Change[root];
//清零标记
Change[root] = 0;
}
return;
}
void Update(int L,int R,int l,int r,int root,int k)
{
if(L<=l&&R>=r){
sum[root] += (r - l + 1)*k;//直接修改大区间保证上层结果正确
Change[root] += k;//叠加延迟标记,保证下层结果
return;
}
int m = (l+r)/2 ;
PushDown(l,r,root);//下推标记先更新,再递归
if(L<=m)Update(L,R,l,m,root<<1,k);
if(R>m)Update(L,R,m+1,r,root<<1|1,k);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
int Query(int L,int R,int l,int r,int rt){//L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
if(L <= l && r <= R){
//在区间内,直接返回
return Sum[rt];
}
int m=(l+r)>>1;
//下推标记,否则Sum可能不正确
PushDown(rt,m-l+1,r-m);
//累计答案
int ANS=0;
if(L <= m) ANS+=Query(L,R,l,m,rt<<1);
if(R > m) ANS+=Query(L,R,m+1,r,rt<<1|1);
return ANS;
}
3. 示例分析
1. HDU - 1698 Just a Hook
注意:要看清题目是区间累加x,还是区间都修改为x;一个是+=x,一个是=x,区别还是很大的。这里就是区间修改为x
#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 100000 + 10;
int sum[maxn<<2];
int Change[maxn<<2];
int len,q;
void PushDown(int l,int r,int root)
{
if(Change[root]){
//下推标记
Change[root<<1] = Change[root];
Change[root<<1|1] = Change[root];
//更新子节点
int m = (l+r)/2;
sum[root<<1] = (m - l + 1)*Change[root];
sum[root<<1|1] = (r - m)*Change[root];
//清零标记
Change[root] = 0;
}
return;
}
void CreatTree(int l,int r,int root)
{
if(l==r){
sum[root] = 1;
return;
}
int m = (l+r)/2;
CreatTree(l,m,root<<1);
CreatTree(m+1,r,root<<1|1);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
void Update(int L,int R,int l,int r,int root,int k)
{
if(L<=l&&R>=r){
sum[root] = (r - l + 1)*k;
Change[root] = k;//延迟标记
return;
}
int m = (l+r)/2 ;
PushDown(l,r,root);
if(L<=m)Update(L,R,l,m,root<<1,k);
if(R>m)Update(L,R,m+1,r,root<<1|1,k);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
int main()
{
int T;
scanf("%d",&T);
int t = 0;
while(T--){
t++;
memset(Change,0,sizeof(Change));
scanf("%d",&len);
CreatTree(1,len,1);
scanf("%d",&q);
for(int i = 1;i<=q;i++){
int x,y,op;
scanf("%d%d%d",&x,&y,&op);
Update(x,y,1,len,1,op);
}
printf("Case %d: The total value of the hook is %d.\n",t,sum[1]);
}
return 0;
}
2. Can you answer these queries? HDU - 4027
(1)题意:修改区间 [ L, R] 的数字开方,查询区间和
(2)分析:woc?区间修改是对每个数字不同处理?区间修改代码怎么做。这时我们可以暴力递归来解决区间修改但是不同处理的情况,但是当一个区间sum[l,r] = l - r + 1时,说明这里的所有数字都变成1了,不用修改了,可以用这个来加快速度,否则超时。
(3)代码实现:
#include <iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
const int maxn = 100000 + 10;
LL sum[maxn<<2];
LL num[maxn];
int n;
void BuildTree(int l,int r,int root)
{
if(l==r){
sum[root] = num[l];
return;
}
int m = (l+r)/2;
BuildTree(l,m,root<<1);
BuildTree(m+1,r,root<<1|1);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
LL Query(int L,int R,int l,int r,int root)
{
if(L<=l&&R>=r){
return sum[root];
}
int m = (l+r)/2;
LL ans = 0;
if(L<=m)ans+=Query(L,R,l,m,root<<1);
if(R>m)ans+=Query(L,R,m+1,r,root<<1|1);
return ans;
}
void Update(int L,int R,int l,int r,int root)
{
if(l==r){
sum[root] = (LL)sqrt(sum[root]);
return;
}
if(L<=l&&R>=r&&sum[root]==r - l +1)return;
int m = (l+r)/2;
if(L<=m)Update(L,R,l,m,root<<1);
if(R>m)Update(L,R,m+1,r,root<<1|1);
sum[root] = sum[root<<1] + sum[root<<1|1];
}
int main()
{
int t = 0;
while(scanf("%d",&n)!=EOF){
t++;
for(int i = 1;i<=n;i++){
scanf("%lld",&num[i]);
}
BuildTree(1,n,1);
int m;
scanf("%d",&m);
printf("Case #%d:\n",t);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(b>c)swap(b,c);
if(a){
LL k = Query(b,c,1,n,1);
printf("%lld\n",k);
}
else{
Update(b,c,1,n,1);
}
}
printf("\n");
}
return 0;
}