搞懂线段树

来源:互联网 发布:网络剧宣传发行方案 编辑:程序博客网 时间:2024/04/29 13:22

引用请注明出处: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,这个很容易验证,自己可以试试。接下来就要考虑有哪些操作了,一般来说有三个操作:建树、更新、查询。

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

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) ;elseupdate(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[]。其实更好的写法是用链表实现,这样每个链表里就可以包含多个属性了,我们把上面的例子用链表再写一下,目的是为了搞明白,而不是给大家提供模板!因为给的代码我也是现场写的。

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));}

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


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



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


原创粉丝点击