线段树是一种 二叉搜索树,与 区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找 某一个节点在若干条线段中出现的次数 ,时间复杂度为O(logN)。而未优化的 空间复杂度 为2N,因此有时需要离散化让空间压缩。
对于线段树中 的每一个非叶子节点[a,b] ,它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是 平衡二叉树 ,最后的子节点数目为N,即整个线段区间的长度。
上图是一棵典型的线段树,它对区间[1,10]进行分割,直到单个点。这棵树的特点 是:
1. 每一层都是区间[a, b]的一个划分,记 L = b - a
2. 一共有log2L层
3. 给定一个点p,从根到叶子p上的所有区间都包含点p,且其他区间都不包含点p。
线段树至少支持下列操作:
Insert(t,x):将包含在区间 int 的元素 x 插入到树t中;
Delete(t,x):从线段树 t 中删除元素 x;
Search(t,x):返回一个指向树 t 中元素 x 的指针。
【以下以 求区间最大值为例】
#include <stdio.h>
#include <math.h>
const int MAXNODE = 2097152;
const int MAX = 1000003;
struct NODE{
int value; // 结点对应区间的权值
int left,right; // 区间 [left,right]
}node[MAXNODE];
int father[MAX]; // 每个点(当区间长度为0时,对应一个点)对应的结构体数组下标
创建线段树(初始化:
由于线段树是用二叉树结构储存的,而且是近乎完全二叉树的,所以在这里我使用了数组来代替链表上图中区间上面的红色数字表示了结构体数组中对应的下标。
在完全二叉树中假如一个结点的序号(数组下标)为 I ,那么 (二叉树基本关系)
I 的父亲为 I/2,
I 的另一个兄弟为 I/2*2 或 I/2*2+1
I 的两个孩子为 I*2 (左) I*2+1(右)
void BuildTree(int i,int left,int right){ // 为区间[left,right]建立一个以i为祖先的线段树,i为数组下标,我称作结点序号
node[i].left = left; // 写入第i个结点中的 左区间
node[i].right = right; // 写入第i个结点中的 右区间
node[i].value = 0; // 每个区间初始化为 0
if (left == right){ // 当区间长度为 0 时,结束递归
father[left] = i; // 能知道某个点对应的序号,为了更新的时候从下往上一直到顶
return;
}
// 该结点往 左孩子的方向 继续建立线段树,线段的划分是二分思想,如果写过二分查找的话这里很容易接受
// 这里将 区间[left,right] 一分为二了
BuildTree(i<<1, left, (int)floor( (right+left) / 2.0));
// 该结点往 右孩子的方向 继续建立线段树
BuildTree((i<<1) + 1, (int)floor( (right+left) / 2.0) + 1, right);
}
【单点更新线段树】:
由于我事先用 father[ ] 数组保存过 每单个结点 对应的下标了,因此我只需要知道第几个点,就能知道这个点在结构体中的位置(即下标)了,这样的话,根据之前已知的基本关系,就只需要直接一路更新上去即可。
void UpdataTree(int ri){ // 从下往上更新(注:这个点本身已经在函数外更新过了)
if (ri == 1)return; // 向上已经找到了祖先(整个线段树的祖先结点 对应的下标为1)
int fi = ri / 2; // ri 的父结点
int a = node[fi<<1].value; // 该父结点的两个孩子结点(左)
int b = node[(fi<<1)+1].value; // 右
node[fi].value = (a > b)?(a):(b); // 更新这个父结点(从两个孩子结点中挑个大的)
UpdataTree(ri/2); // 递归更新,由父结点往上找
}
【查询区间最大值】:
将一段区间按照建立的线段树从上往下一直拆开,直到存在有完全重合的区间停止。对照图例建立的树,假如查询区间为 [2,5]
红色的区间为完全重合的区间,因为在这个具体问题中我们只需要比较这 三个区间的值 找出 最大值 即可。
int Max = -1<<20;
void Query(int i,int l,int r){ // i为区间的序号(对应的区间是最大范围的那个区间,也是第一个图最顶端的区间,一般初始是 1 啦)
if (node[i].left == l && node[i].right == r){ // 找到了一个完全重合的区间
Max = (Max < node[i].value)?node[i].value:(Max);
return ;
}
i = i << 1; // get the left child of the tree node
if (l <= node[i].right){ // 左区间有涉及
if (r <= node[i].right) // 全包含于左区间,则查询区间形态不变
Query(i, l, r);
else // 半包含于左区间,则查询区间拆分,左端点不变,右端点变为左孩子的右区间端点
Query(i, l, node[i].right);
}
i += 1; // right child of the tree
if (r >= node[i].left){ // 右区间有涉及
if (l >= node[i].left) // 全包含于右区间,则查询区间形态不变
Query(i, l, r);
else // 半包含于左区间,则查询区间拆分,与上同理
Query(i, node[i].left, r);
}
}
http://acm.hdu.edu.cn/showproblem.php?pid=1166
/*
HDU 1166 线段树版
对于线段树中的每一个非叶子节点[a,b],
它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
*/
#include<iostream>
#include<stdio.h>
using namespace std;
#define N 50005
int map[N];
int n;
struct node{
int left,right,sum;
};
node g[150010];
void build(int left,int right,int i)
{
int mid;
g[i].left=left;
g[i].right=right;
if(left==right)
{
g[i].sum=map[left];
return ;
}
mid=(left+right)/2;
build(left,mid,2*i);//左节点
build(mid+1,right,2*i+1);//右节点
g[i].sum=g[2*i].sum+g[2*i+1].sum;
}
void insert(int id,int num,int i)
{
if(g[i].left==g[i].right)
{
g[i].sum=g[i].sum+num;
return ;
}
else
{
g[i].sum=g[i].sum+num;
if(id<=g[2*i].right)
insert(id,num,2*i);
else
insert(id,num,2*i+1);
}
}
int search(int l,int r,int k)
{
int mid;
if(g[k].left==l&&g[k].right==r)
return g[k].sum;
mid=(g[k].left+g[k].right)/2;
if(r<=mid)
search(l,r,2*k);
else if(l>mid)
search(l,r,2*k+1);
else
return search(l,mid,2*k)+search(mid+1,r,2*k+1);
}
int main()
{
int t,k,i,a,b;
char str[6];
//freopen("test.txt","r",stdin);
scanf("%d",&t);
k=1;
while(t--)
{
scanf("%d",&n);
for(i=1;i<=n;i++)
scanf("%d",&map[i]);
build(1,n,1);
printf("Case %d:\n",k++);
while(scanf("%s",str))
{
if(str[0]=='E') break;
scanf("%d%d",&a,&b);
if(str[0]=='A')
insert(a,b,1);
else if(str[0]=='S')
insert(a,-b,1);
else if(str[0]=='Q')
{
printf("%d\n",search(a,b,1));
}
}
}
return 0;
}
单点位置的更新
http://hihocoder.com/problemset/problem/1077?sid=421447
/*
对于线段树中的每一个非叶子节点[a,b],
它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
*/
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
#define M 100000005
#define INF 0x7ffffff
struct tree{
int left,right,min;
};
tree g[M];
int map[M];
int getMin(int a,int b)
{
return a>b?b:a;
}
void buildTree(int left,int right,int i)
{
int mid;
g[i].left=left;
g[i].right=right;
if(left==right)
{
g[i].min=map[left];
return ;
}
mid=(left+right)/2;
buildTree(left,mid,2*i);
buildTree(mid+1,right,2*i+1);
g[i].min=getMin(g[2*i].min,g[2*i+1].min);
}
void insert(int id,int num,int i)
{
if(g[i].left==g[i].right)
{
g[i].min=num;
return ;
}
int mid=(g[i].left+g[i].right)/2;
if(id<=mid)
insert(id,num,2*i);
else
insert(id,num,2*i+1);
g[i].min=getMin(g[2*i].min,g[2*i+1].min);
}
int search(int l,int r,int k)
{
int mid;
if(l==g[k].left && r==g[k].right)
return g[k].min;
mid=(g[k].left+g[k].right)/2;
if(r<=mid)
search(l,r,2*k);
else if(l>mid)
search(l,r,2*k+1);
else
return getMin(search(l,mid,2*k),search(mid+1,r,2*k+1));
}
int main()
{
int i,m,n,f,l,r,id,price;
while(scanf("%d",&n)!=EOF)
{
for(i=1;i<=n;i++)
scanf("%d",&map[i]);
buildTree(1,n,1);
scanf("%d",&m);
while(m--)
{
scanf("%d",&f);
if(f==1)
{
scanf("%d%d",&id,&price);
insert(id,price,1);
}
else
{
scanf("%d%d",&l,&r);
printf("%d\n",search(l,r,1));
}
}
}
return 0;
}
区间段的更新
延迟标记概念,这也是线段树的精华所在。
延迟标记:每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。
因此需要在线段树结构中加入延迟标记域,本文例子中我们加入标记与addMark,表示节点的子孙节点在原来的值的基础上加上addMark的值,同时还需要修改创建函数build 和 查询函数 query,修改的代码用红色字体表示,其中区间更新的函数为update
const int INFINITE = INT_MAX;
2 const int MAXNUM = 1000;
3 struct SegTreeNode
4 {
5 int val;
6 int addMark;//延迟标记
7 }segTree[MAXNUM];//定义线段树
8
9
16 void build(int root, int arr[], int istart, int iend)
17 {
18 segTree[root].addMark = 0;//----设置标延迟记域
19 if(istart == iend)//叶子节点
20 segTree[root].val = arr[istart];
21 else
22 {
23 int mid = (istart + iend) / 2;
24 build(root*2+1, arr, istart, mid);//递归构造左子树
25 build(root*2+2, arr, mid+1, iend);//递归构造右子树
26 //根据左右子树根节点的值,更新当前根节点的值
27 segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
28 }
29 }
30
31
35 void pushDown(int root)
36 {
37 if(segTree[root].addMark != 0)
38 {
39 //设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
40 //所以是 “+=”
41 segTree[root*2+1].addMark += segTree[root].addMark;
42 segTree[root*2+2].addMark += segTree[root].addMark;
43 //根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
44 //素加上一个值时,区间的最小值也加上这个值
45 segTree[root*2+1].val += segTree[root].addMark;
46 segTree[root*2+2].val += segTree[root].addMark;
47 //传递后,当前节点标记域清空
48 segTree[root].addMark = 0;
49 }
50 }
51
52
58 int query(int root, int nstart, int nend, int qstart, int qend)
59 {
60 //查询区间和当前节点区间没有交集
61 if(qstart > nend || qend <<span style="font-family: 'Courier New' !important;"> nstart)
62 return INFINITE;
63 //当前节点区间包含在查询区间内
64 if(qstart <= nstart && qend >= nend)
65 return segTree[root].val;
66 //分别从左右子树查询,返回两者查询结果的较小值
67 pushDown(root); //----延迟标志域向下传递
68 int mid = (nstart + nend) / 2;
69 return min(query(root*2+1, nstart, mid, qstart, qend),
70 query(root*2+2, mid + 1, nend, qstart, qend));
71
72 }
73
74
81 void update(int root, int nstart, int nend, int ustart, int uend, int addVal)
82 {
83 //更新区间和当前节点区间没有交集
84 if(ustart > nend || uend <<span style="font-family: 'Courier New' !important;"> nstart)
85 return ;
86 //当前节点区间包含在更新区间内
87 if(ustart <= nstart && uend >= nend)
88 {
89 segTree[root].addMark += addVal;
90 segTree[root].val += addVal;
91 return ;
92 }
93 pushDown(root); //延迟标记向下传递
94 //更新左右孩子节点
95 int mid = (nstart + nend) / 2;
96 update(root*2+1, nstart, mid, ustart, uend, addVal);
97 update(root*2+2, mid+1, nend, ustart, uend, addVal);
98 //根据左右子树的值回溯更新当前节点的值
99 segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
100 }
区间更新举例说明:当我们要对区间[0,2]的叶子节点增加2,利用区间查询的方法从根节点开始找到了非叶子节点[0-2],把它的值设置为1+2 = 3,并且把它的延迟标记设置为2,更新完毕;当我们要查询区间[0,1]内的最小值时,查找到区间[0,2]时,发现它的标记不为0,并且还要向下搜索,因此要把标记向下传递,把节点[0-1]的值设置为2+2 = 4,标记设置为2,节点[2-2]的值设置为1+2 = 3,标记设置为2(其实叶子节点的标志是不起作用的,这里是为了操作的一致性),然后返回查询结果:[0-1]节点的值4;当我们再次更新区间[0,1](增加3)时,查询到节点[0-1],发现它的标记值为2,因此把它的标记值设置为2+3 = 5,节点的值设置为4+3 = 7;
其实当区间更新的区间左右值相等时([i,i]),就相当于单节点更新,单节点更新只是区间更新的特例。
http://hihocoder.com/problemset/problem/1078?sid=421630
/*
对于线段树中的每一个非叶子节点[a,b],
它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
*/
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
#define M 1000005
struct tree{
int left,right,sum,lazy;
};
tree g[M];
int map[M];
void pushDown(int i)
{
if(g[i].lazy)
{
g[2*i].lazy=1;
g[2*i+1].lazy=1;
g[i].lazy=0;
g[2*i].sum=g[i].sum/(g[i].right-g[i].left+1)*(g[2*i].right-g[2*i].left+1);
g[2*i+1].sum=g[i].sum/(g[i].right-g[i].left+1)*(g[2*i+1].right-g[2*i+1].left+1);
}
}
void buildTree(int left,int right,int i)
{
int mid;
g[i].lazy=0;
g[i].left=left;
g[i].right=right;
if(left==right)
{
g[i].sum=map[left];
return ;
}
mid=(left+right)/2;
buildTree(left,mid,2*i);
buildTree(mid+1,right,2*i+1);
g[i].sum=g[2*i].sum+g[2*i+1].sum;
}
void insert(int l,int r,int num,int i)
{
if(g[i].lazy) pushDown(i);
if(l==g[i].left && g[i].right==r)
{
g[i].sum=(g[i].right-g[i].left+1)*num;
g[i].lazy=1;
return ;
}
int mid=(g[i].left+g[i].right)/2;
if(r<=mid)
insert(l,r,num,2*i);
else if(mid<l)
insert(l,r,num,2*i+1);
else
{
insert(l,mid,num,2*i);
insert(mid+1,r,num,2*i+1);
}
g[i].sum=g[2*i].sum+g[2*i+1].sum;
}
int search(int l,int r,int k)
{
int mid;
if(g[k].lazy) pushDown(k);
if(l==g[k].left && r==g[k].right)
return g[k].sum;
mid=(g[k].left+g[k].right)/2;
if(r<=mid)
search(l,r,2*k);
else if(l>mid)
search(l,r,2*k+1);
else
return search(l,mid,2*k)+search(mid+1,r,2*k+1);
}
int main()
{
int i,m,n,f,l,r,price;
while(scanf("%d",&n)!=EOF)
{
for(i=1;i<=n;i++)
scanf("%d",&map[i]);
buildTree(1,n,1);
scanf("%d",&m);
while(m--)
{
scanf("%d",&f);
if(f==1)
{
scanf("%d%d%d",&l,&r,&price);
insert(l,r,price,1);
}
else
{
scanf("%d%d",&l,&r);
printf("%d\n",search(l,r,1));
}
}
}
return 0;
}