线段树学习笔记

数据结构--线段树
一.线段树介绍:
    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
    对于线段树中的每一个非叶子节点[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)呢?这也保证了我只是更新到我需要的区间,避免一直更新到叶子结点,造成大量时间开销

到此,线段树的以基本应用已经讲完了,可以多练几个题,加深理解,为后面的线段树更高级的应用区间合并和扫描线打下基础




三,区间合并,扫描线
此处待续。。。。。。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值