出处: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) ;
- 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[]。其实更好的写法是用链表实现,这样每个链表里就可以包含多个属性了,我们把上面的例子用链表再写一下,目的是为了搞明白,而不是给大家提供模板!因为给的代码我也是现场写的。
- 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));
- }
其实,写法都差不多,记住:不是为了给模板,只是为了帮助理解,因为写的确实不是很养眼。。。
其实,如果把上面的都理解的差不多的话,线段树算基本掌握了吧。但是,还有个东西,有必要提一下:在进行区间更新的时候,有个懒操作的技巧。解释一下:就是每个节点增加了一个属性,用来记录这个对应的区间是否被更新过(或更新过多少)。这样,在每次更新操作的时候不用马上一直递归更新下去,只有等到下次更新或查询的时候才顺带的更新到下一级。这个实现的话,就是在链表里增加一个属性,每次更新或查询的时候都要判断是否被标记(即之前这个区间是否被更新过),如果标记有效,则把它的两个儿子节点对应的区间也标记了。这其实也是一种平均的作用,避免“极端分子”。
差不多了吧,就写到这吧,有点感冒不舒服,以后如果想到什么再添加吧~