搞懂线段树

引用请注明出处:http://blog.csdn.net/int64ago/article/details/7506179



前段时间写了篇“搞懂树状数组”,如果说树状数组是优雅的,那么线段树就是万能的。有句话就叫:树状数组能做的线段树都能做,但是树状数组能做的坚决用树状数组!因为线段树本来的内容狠丰富的,主要有单点跟新、区间跟新,最值询问、区间询问…………反正就是对于区间进行的动态跟新询问都能较高效的完成。

对于初学者,一定要明白一点:线段树就是一种可以较高效的动态的对区间进行跟新查询!至于它具体能干哪些事取决于你树里所存储的信息量!不要一开始就想要什么通用的模板,模板可能有,但是如果你不理解它,恐怕也用的不顺心。其实,线段树的实现本省代码量不是很大,复杂性也不是很高,完全可以每次根据题目具体的需求写出来。当然,前提是自己理解了。先来看一幅图:

它就是一颗完全二叉树,根节点是一个大区间,两个儿子节点是根节点区间一分为二。当然,最重要的东西图中并没有体现出来,它只是描述了节点之间的逻辑关系,而我没实现它的时候一般都要为节点(即所表示的区间)附加点属性:最大值、最小值、和。。。。

现在我们就要了解如何实现它了,如果每个节点只有一个简单的附加属性,完全可以用数组实现。我们对上面的节点从上到下、从左到右一次标号1、2、……、M,可以看出节点1的两个儿子分别是2和3,以此类推节点i的左右儿子分别是2*i和2*i+1,用位运算可以表示为i<<1和i<<1|1,所以我们可以用一个数组tree[]来表示这个附加属性,而下表则是节点的标号。那么,这个数组申请多大合适呢?根据树的理论,如果区间范围是[0,N-1],则M=2*N+1,这个很容易验证,自己可以试试。接下来就要考虑有哪些操作了,一般来说有三个操作:建树、更新、查询。

比如对于单点更新,查询某个区间的最大值,可以用下面的代码简单实现

  1. int tree[2*MAX_N+1];  
  2.   
  3. /*建立以k为根节点[L,H]为操作区间的线段树*/  
  4. void built_tree(int k, int L, int H)  
  5. {  
  6.     if(L == H){  
  7.         scanf("%d", &tree[k]);  
  8.         return ;  
  9.     }  
  10.     built_tree(k << 1, L, (L + H) << 1);  
  11.     built_tree(k << 1 | 1, (L + H) << 1 | 1, H);  
  12. }  
  13.   
  14. /*在根节点为k,[L,H]为操作区间的线段树里对id处的值更新为key*/  
  15. int update_tree(int k, int L, int H, int id, int key)  
  16. {  
  17.     if(L == H){  
  18.         tree[k] = key;  
  19.         return ;  
  20.     }  
  21.     if(id < (L + H)/2)  
  22.         update(k*2, L, (L + H)/2, id, key) ;  
  23.     else  
  24.         update(K*2 + 1, (L + H)/2 + 1, H, id, key);  
  25.     tree[k] = MAX(tree[k*2], tree[2*k + 1]);  
  26. }  
  27.   
  28. /*在根节点为k,[L,H]为操作区间的线段树里查询区间[beg,end]的最大值*/  
  29. int read_tree(int k, int L, int H, int beg, int end)  
  30. {  
  31.     if(beg > H || end < L) return -INT_MAX;  
  32.     if(beg <= L && end >= H) return tree[k];  
  33.     return MAX(read_tree(2*k, L, (L + H)/2, beg, end),  
  34.             read_tree(2*k + 1, (L + H)/2 + 1, H, beg, end));  
  35. }  
int tree[2*MAX_N+1];

/*建立以k为根节点[L,H]为操作区间的线段树*/
void built_tree(int k, int L, int H)
{
	if(L == H){
		scanf("%d", &tree[k]);
		return ;
	}
	built_tree(k << 1, L, (L + H) << 1);
	built_tree(k << 1 | 1, (L + H) << 1 | 1, H);
}

/*在根节点为k,[L,H]为操作区间的线段树里对id处的值更新为key*/
int update_tree(int k, int L, int H, int id, int key)
{
	if(L == H){
		tree[k] = key;
		return ;
	}
	if(id < (L + H)/2)
		update(k*2, L, (L + H)/2, id, key) ;
	else
		update(K*2 + 1, (L + H)/2 + 1, H, id, key);
	tree[k] = MAX(tree[k*2], tree[2*k + 1]);
}

/*在根节点为k,[L,H]为操作区间的线段树里查询区间[beg,end]的最大值*/
int read_tree(int k, int L, int H, int beg, int end)
{
	if(beg > H || end < L) return -INT_MAX;
	if(beg <= L && end >= H) return tree[k];
	return MAX(read_tree(2*k, L, (L + H)/2, beg, end),
			read_tree(2*k + 1, (L + H)/2 + 1, H, beg, end));
}

用的是递归实现的,复杂度都是O(lgn),可能有人就疑问了,为什么用线段树就能节省时间呢?其实也不能算节省,它把时间平均了,避免了“尖端分子”影响整个性能!比如,如果我们不用线段树,用一段方法实现的话,那么更新操作其实是O(1)的复杂度,而查询和建树就变成了O(n)了。还有,线段树也多牺牲了点内存,这就是有得必有失嘛~


上面的例子是每个节点只有一个属性的情况,加入现在有多个属性呢?那么tree[]的值表示什么呢?除非你再定义一个tree_other[]。其实更好的写法是用链表实现,这样每个链表里就可以包含多个属性了,我们把上面的例子用链表再写一下,目的是为了搞明白,而不是给大家提供模板!因为给的代码我也是现场写的。

  1. typedef struct _Tree{  
  2.     int L, H;  
  3.     struct _Tree *left, *right;  
  4.     int mmax;  
  5.     /* ...other properties */  
  6. }Tree;  
  7.   
  8. Tree *init_tree(int L, int H)  
  9. {  
  10.     Tree *treeP = (Tree *)malloc(sizeof(Tree));  
  11.     treeP->H = H;  
  12.     treeP->L = L;  
  13.     if(L == H){   
  14.         treeP->left = NULL;  
  15.         treeP->right = NULL;  
  16.         scanf("%d", &treeP->mmax);  
  17.     }  
  18.     else{  
  19.         treeP->left = init_tree(L, (L + H)/2);  
  20.         treeP->right = init_tree((L + H)/2 + 1, H);  
  21.         treeP->mmax = MAX(treeP->left->mmax, treeP->right->mmax);  
  22.     }  
  23.     return treeP;  
  24. }  
  25.   
  26. void update(Tree *root, int id, int key)  
  27. {  
  28.     if(root->H == root->L){  
  29.         root->mmax = key;  
  30.         return ;  
  31.     }  
  32.     if(id < (root->H + root->L)/2){  
  33.         update(root->left, id, key);  
  34.     } else{  
  35.         update(root->right, id, key);  
  36.     }  
  37.     root->mmax = MAX(root->left->mmax, root->right->mmax);  
  38. }  
  39.   
  40. int read(Tree *root, int beg, int end)  
  41. {  
  42.     if(beg > root->H || end < root->L) return -INT_MAX;  
  43.     if(beg <= root->L && end >= root->H) return root->mmax;  
  44.     return MAX(read(root->left, beg, end), read(root->right, beg, end));  
  45. }  
typedef struct _Tree{
	int L, H;
	struct _Tree *left, *right;
	int mmax;
	/* ...other properties */
}Tree;

Tree *init_tree(int L, int H)
{
	Tree *treeP = (Tree *)malloc(sizeof(Tree));
	treeP->H = H;
	treeP->L = L;
	if(L == H){ 
		treeP->left = NULL;
		treeP->right = NULL;
		scanf("%d", &treeP->mmax);
	}
	else{
		treeP->left = init_tree(L, (L + H)/2);
		treeP->right = init_tree((L + H)/2 + 1, H);
		treeP->mmax = MAX(treeP->left->mmax, treeP->right->mmax);
	}
	return treeP;
}

void update(Tree *root, int id, int key)
{
	if(root->H == root->L){
		root->mmax = key;
		return ;
	}
	if(id < (root->H + root->L)/2){
		update(root->left, id, key);
	} else{
		update(root->right, id, key);
	}
	root->mmax = MAX(root->left->mmax, root->right->mmax);
}

int read(Tree *root, int beg, int end)
{
	if(beg > root->H || end < root->L) return -INT_MAX;
	if(beg <= root->L && end >= root->H) return root->mmax;
	return MAX(read(root->left, beg, end), read(root->right, beg, end));
}

其实,写法都差不多,记住:不是为了给模板,只是为了帮助理解,因为写的确实不是很养眼。。。


其实,如果把上面的都理解的差不多的话,线段树算基本掌握了吧。但是,还有个东西,有必要提一下:在进行区间更新的时候,有个懒操作的技巧。解释一下:就是每个节点增加了一个属性,用来记录这个对应的区间是否被更新过(或更新过多少)。这样,在每次更新操作的时候不用马上一直递归更新下去,只有等到下次更新或查询的时候才顺带的更新到下一级。这个实现的话,就是在链表里增加一个属性,每次更新或查询的时候都要判断是否被标记(即之前这个区间是否被更新过),如果标记有效,则把它的两个儿子节点对应的区间也标记了。这其实也是一种平均的作用,避免“极端分子”。



差不多了吧,就写到这吧,有点感冒不舒服,以后如果想到什么再添加吧~


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值