数据结构--线段树
一.线段树介绍:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
线段树可以以log(N)的复杂度进行更新,查询。
线段树所需空间一般开到N的4倍即可。
二.线段树的两类基本问题-----单点更新,成段更新
<I>.单点更新:让我们跟着一个例子来感受一下。
题目:HDU 1166 敌兵布阵
第一步,建树---根据题目要求,每个叶子节点代表的是对应点的人数,由于二叉树的特性,我们可以边读边建树。当然如果初始值有一定规律,
也可以采用其他初始方法,如开始全为0,直接memset就行了
第二步,更新---单点更新。所谓的单点更新就是对树上的一个叶子节点的值进行修改,并在递归回来时,对包含该叶子节点的区间进行更新
第三步,查询----区间查询,查询某个点的值就不用说了吧,直接
到此,线段树的主要三步已经完成,这个问题也已经得以解决,为了加深理解,可以做做下面两道题
HDU 1754 --- I Hate it(节点上存的是区间最大值)
HDU 1394 --- Minimum Inversion Number(线段树求逆序对,实际上用线段树求逆序对要先进行离散化,但是这里数字比较特殊,直接是0~n-1,就无需离散化了)
HDU 2795 --- Billboard(区间最大值)
POJ 2828 --- Buy Tickets
POJ 2886 --- Who Gets the Most Candies?
<II>成段更新:(通常这对初学者来说是一道坎),需要用到延迟标记(或者说懒惰标记),简单来说就是每次更新的时候不要更新到底,
用延迟标记使得更新延迟到下次需要更新or询问到的时候
仍然是跟着例题来看
题目:HDU1698 ---- Just a Hook
第一步,建树,此处和单点更新一样,不在多说
第二步,更新操作,对一段区间的值进行更新
第三步,仍然是查询,方法和单点更新一致,这里也不在多说
题目练习:
POJ 3468 --- A Simple Problem with Integers
POJ 2528 --- Mayor’s posters
POJ 3225 --- Help with Intervals
POJ 1436 --- Horizontally Visible Segments
POJ 2991 --- Crane
<III>下面来总结一下
线段树的基本应用就是这两个了,单点更新和成段更新,单点更新比较简单,update函数也比较好理解,query可能有点不好理解
为什么是(x<=le && y >= ri)时返回值呢?我们可以想象一下,我们查询的区间是(x,y),这样写包含了区间(le,ri),而返回的值仍然是(le,ri)区间的和
所以这样写不会影响,而且也十分方便
成段更新update的关键就是pushdown函数,对于区间求和的问题,我们传入的是当前结点区间的长度和编号,既然是求和,那么要想把延迟更新里的标记
正确的传给他的左孩子树和右孩子树,那么左边应该是(m-(m>>1))这么个区间长度乘上col[rt],右边就自然是(m>>1)*col[rt]了。那么为什么要把延迟更新
更新到他的子节点呢?其实也很简单,因为我在此次更新树的过程还用不着下面的结点,我还是可以继续懒得下去,等需要应用深层的区间我再去更新
还有一点,更新为什么是(x <= le && y >= ri)呢?这也保证了我只是更新到我需要的区间,避免一直更新到叶子结点,造成大量时间开销
到此,线段树的以基本应用已经讲完了,可以多练几个题,加深理解,为后面的线段树更高级的应用区间合并和扫描线打下基础
三,区间合并,扫描线
此处待续。。。。。。
一.线段树介绍:
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。
线段树可以以log(N)的复杂度进行更新,查询。
线段树所需空间一般开到N的4倍即可。
二.线段树的两类基本问题-----单点更新,成段更新
<I>.单点更新:让我们跟着一个例子来感受一下。
题目:HDU 1166 敌兵布阵
第一步,建树---根据题目要求,每个叶子节点代表的是对应点的人数,由于二叉树的特性,我们可以边读边建树。当然如果初始值有一定规律,
也可以采用其他初始方法,如开始全为0,直接memset就行了
void built(int le,int ri,int num){ //le代表区间左端点,ri代表区间右端点,num代表节点的编号,num*2是节点num的左孩子,num*2+1是其右孩子
if(le == ri){
scanf("%d",&tree[num]);
return;
}
int mid = (le+ri)/2;
built(le,mid,num*2);
built(mid+1,ri,num*2+1);
tree[num] = tree[num*2]+tree[num*2+1]; //根据二叉树结构和题目要求,想想为什么?(此处是什么操作和题目要求有关)
}
第二步,更新---单点更新。所谓的单点更新就是对树上的一个叶子节点的值进行修改,并在递归回来时,对包含该叶子节点的区间进行更新
void update(int le,int ri,int num,int x,int val) //x代表要更新的节点,val代表更新的值
{
if(le == x && ri == x)
{
tree[num] += val;
return;
}
int mid = (le+ri)/2; //类似二分查找找到x所在节点
if(x <= mid)
update(le,mid,num*2,x,val);
else
update(mid+1,ri,num*2+1,x,val);
tree[num] = tree[num*2]+tree[num*2+1]; //对包含该叶子节点的区间进行更新
}
第三步,查询----区间查询,查询某个点的值就不用说了吧,直接
int query(int le,int ri,int num, int x,int y)
{
if(x <= le && y >= ri) //想想此处为何是这个判断条件而不是(x == le && y == ei)??(可以自己画个线段树好好想想,加深理解)
{
return tree[num];
}
int mid = (le+ri)/2, ans;
if(x <= mid)
ans += query(le,mid,num*2,x,y); //与上面的判断条件对应,查询区间仍然是(x,y)
if(y > mid)
ans += query(mid+1,ri,num*2+1,x,y);
return ans;
}
到此,线段树的主要三步已经完成,这个问题也已经得以解决,为了加深理解,可以做做下面两道题
HDU 1754 --- I Hate it(节点上存的是区间最大值)
HDU 1394 --- Minimum Inversion Number(线段树求逆序对,实际上用线段树求逆序对要先进行离散化,但是这里数字比较特殊,直接是0~n-1,就无需离散化了)
HDU 2795 --- Billboard(区间最大值)
POJ 2828 --- Buy Tickets
POJ 2886 --- Who Gets the Most Candies?
<II>成段更新:(通常这对初学者来说是一道坎),需要用到延迟标记(或者说懒惰标记),简单来说就是每次更新的时候不要更新到底,
用延迟标记使得更新延迟到下次需要更新or询问到的时候
仍然是跟着例题来看
题目:HDU1698 ---- Just a Hook
第一步,建树,此处和单点更新一样,不在多说
第二步,更新操作,对一段区间的值进行更新
void pushdown(int rt,int m)
{
if(col[rt]) //col数组,延迟更新标记,标记着下面的值要更新多少
{
tree[rt<<1] = (m-(m>>1))*col[rt]; //将延迟更新数组里的值更新到树的结点
tree[rt<<1|1] = (m>>1)*col[rt];
col[rt<<1] = col[rt<<1|1] = col[rt]; //将延迟更新数组的值传递给他的子孩子
col[rt] = 0;
}
}
void update(int le,int ri,int rt,int x,int y,int val) //x,y代表要更新的区间,val代表更新的值
{
if(x <= le && y >= ri) //这里只更新到当前区间所在的区间
{
col[rt] = val;
tree[rt] = (ri-le+1)*val;
return;
}
pushdown(rt,ri-le+1); //将延迟更新数组里的值更新到树的结点
int mid = (le+ri)>>1;
if(x <= mid)
update(lson,x,y,val);
if(y > mid)
update(rson,x,y,val);
tree[rt] = tree[rt<<1]+tree[rt<<1|1];
}
第三步,仍然是查询,方法和单点更新一致,这里也不在多说
题目练习:
POJ 3468 --- A Simple Problem with Integers
POJ 2528 --- Mayor’s posters
POJ 3225 --- Help with Intervals
POJ 1436 --- Horizontally Visible Segments
POJ 2991 --- Crane
<III>下面来总结一下
线段树的基本应用就是这两个了,单点更新和成段更新,单点更新比较简单,update函数也比较好理解,query可能有点不好理解
为什么是(x<=le && y >= ri)时返回值呢?我们可以想象一下,我们查询的区间是(x,y),这样写包含了区间(le,ri),而返回的值仍然是(le,ri)区间的和
所以这样写不会影响,而且也十分方便
成段更新update的关键就是pushdown函数,对于区间求和的问题,我们传入的是当前结点区间的长度和编号,既然是求和,那么要想把延迟更新里的标记
正确的传给他的左孩子树和右孩子树,那么左边应该是(m-(m>>1))这么个区间长度乘上col[rt],右边就自然是(m>>1)*col[rt]了。那么为什么要把延迟更新
更新到他的子节点呢?其实也很简单,因为我在此次更新树的过程还用不着下面的结点,我还是可以继续懒得下去,等需要应用深层的区间我再去更新
还有一点,更新为什么是(x <= le && y >= ri)呢?这也保证了我只是更新到我需要的区间,避免一直更新到叶子结点,造成大量时间开销
到此,线段树的以基本应用已经讲完了,可以多练几个题,加深理解,为后面的线段树更高级的应用区间合并和扫描线打下基础
三,区间合并,扫描线
此处待续。。。。。。