一、线段树概念
线段树是一种完全二叉树,它在每个节点保存数组中一段子数组的信息,从而进行高效的连续区间动态查询问题。
二、基本操作
1.构造(初始化)
线段树初始化的主要思想是递归如果一个节点(n)保存的线段长度是1则赋值,否则递归构造左(2*n)右(2*n+1)子节点,最后回溯时给该节点赋值。
2.区间查询
区间查询先把欲查询的区间划分为线段树上的节点,再把这些节点储存的信息整合起来得到需要的信息。
比如这一简单的线段树,如果查询区间是1-4、1-2等,那可以直接找到相应节点保存的信息来得到结果。如果查询区间是2-4,就不能直接得到结果,需要整合2-2、3-4两个节点的信息得到结果13。
由此可见,线段树的查询过程选出了最少的节点数来保证这些节点所表示的区间连起来正好是欲查询的区间。考虑到线段树上每层节点最多被选出两个,因此查询时间复杂度是O(log n)。
3.单节点更新
单节点更新没什么好说的,递归找到要更新的节点,更新节点值,回溯更新父节点直到根节点。
4.区间更新(重点)
区间更新一个重要思想就是,按照查询的过程将待更新区间划分为多个节点,然后更新这些节点的信息,在此次更新操作中不继续更新这些节点的子节点信息,而是在这些节点添加标记。每当修改和查询到一个节点并且需要继续访问它的子节点时,看看它有没有标记,如果有标记,就按照标记更新其左右子节点信息并为左右子节点添加相同的标记,同时删除该节点的标记。这种区间更新方法不需要暴力更新每个节点的值,大大提高效率。
三、样例代码(区间最小值)
#include<iostream>
#include<string>
using namespace std;
struct segtree
{
private:
struct node
{
node()
{
change=0;
}
long long int value;
long long int change;
int begin;
int end;
};
node *tree;
int members;
long long int *buildp;
void build(int n,int begin,int end)//初始化
{
int mid=(begin+end)/2;
tree[n].begin=begin;
tree[n].end=end;
if(begin==end)//搜索到叶子节点,赋值,回溯
{
tree[n].value=*buildp;
++buildp;
return;
}
build(2*n,begin,mid);
build(2*n+1,mid+1,end);//递归构造左右子节点
update(n);
}
long long int query(int n,int begin,int end)//区间查询
{
long long int q1=-1,q2=-1;
int mid=(begin+end)/2;
if(tree[n].begin>end||tree[n].end<begin)//查询区间与当前区间无交集
{
return -1;
}
if(tree[n].begin>=begin&&tree[n].end<=end)//当前区间为查询区间的子集
{
return tree[n].value;
}
if(tree[n].change!=0)//检查是否有标记需要处理
{
sink(n);
}
q1=query(2*n,begin,end);
q2=query(2*n+1,begin,end);
if(q1==-1)
{
return q2;
}
if(q2==-1)
{
return q1;
}
return min(q1,q2);
}
void change(int target,long long int dv,int n)//单点更新
{
if(tree[n].begin==tree[n].end)
{
tree[n].value+=dv;
return;
}
if(tree[n].change!=0)
{
sink(n);
}
int mid=(tree[n].begin+tree[n].end)/2;
if(target<=mid)
{
change(target,dv,2*n);
}
else
{
change(target,dv,2*n+1);
}
update(n);
}
void areachange(int begin,int end,long long int dv,int n)//区域更新
{
if(tree[n].begin>end||tree[n].end<begin)
{
return;
}
if(tree[n].begin>=begin&&tree[n].end<=end)//当前区间是待更新区间子集,添加标记
{
tree[n].change+=dv;
tree[n].value+=tree[n].change;
return;
}
if(tree[n].change!=0)
{
sink(n);
}
areachange(begin,end,dv,2*n);
areachange(begin,end,dv,2*n+1);
update(n);
}
void update(int n)//更新父节点值
{
tree[n].value=min(tree[2*n].value,tree[2*n+1].value);
}
void sink(int n)//标记下沉
{
tree[2*n].change+=tree[n].change;
tree[2*n].value+=tree[2*n].change;
tree[2*n+1].change+=tree[n].change;
tree[2*n+1].value+=tree[2*n+1].change;
tree[n].change=0;
}
public:
void build(long long int *arr,int lenth)
{
tree=new node[lenth*2];
members=lenth;
buildp=arr;
build(1,1,lenth);
}
long long int query(int begin,int end)
{
return query(1,begin,end);
}
void change(int target,long long int dv)
{
change(target,dv,1);
}
void areachange(int begin,int end,long long int dv)
{
areachange(begin,end,dv,1);
}
};
四、例题
1.hdu 1166
这是一题很裸的线段树,而且不需要用到区间求和,更加简单。
#include<string>
#include<cstdio>
#include<cstring>
using namespace std;
struct segtree
{
private:
struct node//节点结构体,用来保存信息
{
node()
{
change=0;
}
long long int value;
long long int change;
int begin;
int end;
};
node *tree;
int members;
long long int *buildp;
void build(int n,int begin,int end)
{
int mid=(begin+end)/2;
tree[n].begin=begin;
tree[n].end=end;
if(begin==end)//长度为1,赋值
{
tree[n].value=*buildp;
++buildp;
return;
}
build(2*n,begin,mid);
build(2*n+1,mid+1,end);//递归构造左右子节点
update(n);
}
long long int query(int n,int begin,int end)
{
long long int q1=0,q2=0;
int mid=(begin+end)/2;
if(tree[n].begin>end||tree[n].end<begin)//查找区间和当前区间无交集
{
return 0;
}
if(tree[n].begin>=begin&&tree[n].end<=end)//当前区间为查找区间子集
{
return tree[n].value;
}
q1=query(2*n,begin,end);
q2=query(2*n+1,begin,end);
return q1+q2;
}
void change(int target,long long int dv,int n)
{
if(tree[n].begin==tree[n].end)//找到待更新节点,修改信息
{
tree[n].value+=dv;
return;
}
int mid=(tree[n].begin+tree[n].end)/2;
if(target<=mid)//未找到待更新节点,寻找正确的子节点继续寻找
{
change(target,dv,2*n);
}
else
{
change(target,dv,2*n+1);
}
update(n);
}
void update(int n)
{
tree[n].value=tree[2*n].value+tree[2*n+1].value;
}
public:
void build(long long int *arr,int lenth)//初始化
{
tree=new node[lenth*4];
members=lenth;
buildp=arr;
build(1,1,lenth);
}
long long int query(int begin,int end)//区间查询
{
return query(1,begin,end);
}
void change(int target,long long int dv)//单点更新
{
change(target,dv,1);
}
};
int main()
{
int t,tn=1;
scanf("%d",&t);
while(tn<=t)
{
int n;
scanf("%d",&n);
long long int *arr=new long long int[n];
for(int i=0;i<n;++i)
{
scanf("%d",&arr[i]);
}
printf("Case %d:\n",tn);
++tn;
segtree tree;
tree.build(arr,n);
char command[6];
long long int in1,in2;
while(scanf("%s",command))
{
if(strcmp(command,"End")==0)
{
break;
}
else if(strcmp(command,"Query")==0)
{
scanf("%ld%ld",&in1,&in2);
long long int rst=tree.query(in1,in2);
printf("%ld\n",rst);
}
else if(strcmp(command,"Add")==0)
{
scanf("%ld%ld",&in1,&in2);
tree.change(in1,in2);
}
else if(strcmp(command,"Sub")==0)
{
scanf("%ld%ld",&in1,&in2);
tree.change(in1,-in2);
}
}
}
return 0;
}
2.hdu 1698
这道题用到了区间查询,因为查询的是整条的和,所以只要输出第一个节点的value就行。而且这题我之前写的那种使用longlong并且把线段开始和结束的地址都存进节点的做法会mle,因此这里我把线段部分信息用函数参数传递,能节省空间。(p.s cin和cout会tle)
#include<iostream>
#include<string>
#include<cstdio>
using namespace std;
struct segtree
{
private:
struct node//只保存节点值和变化标记
{
node()
{
change=0;
}
int value;
int change;
};
node *tree;
int members;
int *buildp;
void build(int n,int begin,int end)
{
int mid=(begin+end)/2;
if(begin==end)
{
tree[n].value=*buildp;
++buildp;
return;
}
build(2*n,begin,mid);
build(2*n+1,mid+1,end);
update(n);
}
int query(int n,int begin,int end,int bn,int en)//bn,en表示当前访问的节点区间的起始结束位置
{
int q1=0,q2=0;
int mid=(begin+end)/2;
int midn=(bn+en)/2;
if(bn>end||en<begin)
{
return 0;
}
if(bn>=begin&&en<=end)
{
return tree[n].value;
}
if(tree[n].change!=0)
{
sink(n,bn,en);
}
q1=query(2*n,begin,end,bn,midn);
q2=query(2*n+1,begin,end,midn+1,en);
return q1+q2;
}
void areachange(int begin,int end,int dv,int n,int bn,int en)
{
if(bn>end||en<begin)
{
return;
}
if(bn>=begin&&en<=end) //当前区间为待更新区间的子集,添加标记
{
tree[n].change=dv;
tree[n].value=tree[n].change*(en-bn+1);
return;
}
if(tree[n].change!=0)
{
sink(n,bn,en);
}
int midn=(bn+en)/2;
areachange(begin,end,dv,2*n,bn,midn);
areachange(begin,end,dv,2*n+1,midn+1,en);
update(n);
}
void update(int n)
{
tree[n].value=tree[2*n].value+tree[2*n+1].value;
}
void sink(int n,int bn,int en)//因为这题是用新值覆盖旧值,因此更新节点时直接用=就行
{
int midn=(bn+en)/2;
tree[2*n].change=tree[n].change;
tree[2*n].value=tree[2*n].change*(midn-bn+1);
tree[2*n+1].change=tree[n].change;
tree[2*n+1].value=tree[2*n+1].change*(en-midn);
tree[n].change=0;
}
public:
void build(int *arr,int lenth)
{
tree=new node[lenth*4];
members=lenth;
buildp=arr;
build(1,1,lenth);
}
int query(int begin,int end)
{
return query(1,begin,end,1,members);
}
void areachange(int begin,int end,int dv)
{
areachange(begin,end,dv,1,1,members);
}
};
int main()
{
int t,t0=1;
scanf("%d",&t);
while(t0<=t)
{
int n,m;
int *arr;
scanf("%d%d",&n,&m);
arr=new int[n];
for(int i=0;i<n;++i)
{
arr[i]=1;//一开始默认铜牌,初值为1
}
segtree tree;
tree.build(arr,n);
for(int i=0;i<m;++i)
{
int bg,en,cg;
scanf("%d%d%d",&bg,&en,&cg);
tree.areachange(bg,en,cg);
}
int rst=tree.query(1,n);
printf("Case %d: The total value of the hook is %d.\n",t0,rst);
++t0;
}
return 0;
}