线段树


  1. 参考:

    1. 算法训练营

    2. 大佬笔记:https://zhuanlan.zhihu.com/p/106118909

  2. 简介:

    1. 名称:线段树(Segment Tree)

    2. 本质:维护区间信息(信息需要满足结合律,就是(a+b)+c=a+(b+c)的那种性质),对于点、区间更新、区间查询的复杂度均为O(logn)。

    3. 一些abstract:线段树是一棵平衡二叉树,每个节点代表一个区间内的值,以区间和为例。母节点比如代表[l, r]的和,左儿子就是[l, l+mid]的和,右儿子就是[l+mid+1, r]的和。线段树比树状数组可以多实现O(logn)的区间修改,支持区间加、乘等,更具通用性,也就是说能用树状数组解决的问题都可以用线段树解决,但是树状数组节省空间,代码易懂,运行常数小,所以二者各有千秋。

       

  3. 操作:以最简单的区间最大值为例:

    1. 创建Build:lc为左儿子,rc为右儿子。显然要递归建树,根据儿子的结果得到父亲的结果

      板子:

       #define lc k*2
       #define rc k*2+1
       #define inf 0x3f3f3f3f
       
       // 区间最大值
       void build(int k, int l, int r) {
         tree[k].l;
         tree[k].r;
         if (== r) {
           tree[x].ans a[l];
           return;
        }
         build(lc, l, mid);
         build(rc, mid+1, r);
         tree[k].ans max(tree[lc].ans, tree[rc].ans);
       }
    2. 点更新update:现在将a[i]修改为v,显然也是先改叶子节点,然后一步步往上修改到根节点。

      板子:

       void update(int k, int i, int v) {  // 将a[i]更新为v
         if (tree[k].== tree[k].&& tree[k].== i) {  // 有一说一后面的部分纯属扯淡,因为i大于建树的n应该直接特判出来,所以后面那个默认是承认的
           tree[i].ans v;
           return;
        }
         int mid = (tree[k].tree[k].r) 2;
         if (<= mid) update(lc, i, v);  // 划分到左子树中
         else update(rc, i, v);  // 划分到右子树中
         tree[k].ans max(tree[lc].ans, tree[rc].ans);  // 得到了儿子们的答案,
       }
    3. 区间查询:查询[l, r]区间的最值,显然不会有刚刚好的区间让你去查询,看起来得按照建的树,分成若干个小线段,逐步渗透到叶子节点,比如刚才的树中,如果查询[2, 4]的最值,因为在3这个位置就已经裂开了,所以显然要分成[2, 3], [4]两个子线段,然后把[2, 3]下放到左子树,[4]下放到右子树。左子树中因为2这个位置会裂开,所以又得分成[2]和[3]分别送到叶子节点,右侧的[4]也送到叶子节点,然后得到答案之后取个max就行了。

      板子:

       int query(int k, int l, int r) {  // 求[l, r]区间的最值,l和r在整个查询过程中是一定的,改变的是k,而k对应了线段树中的左右区间
         if (tree[k].>= && tree[k].<= r) {  // 直接包括了整个k代表的区间,所以要返回k的max
           return tree[k].ans;
        }
         int mid = (tree[k].tree[k].r) 2;
         int Max -inf;
         if (<= mid) {  // 涉及到左子树的答案,递归查询
           Max max(Max, query(lc, l, r));
        }
         if (mid) {  // 涉及到右子树的答案,递归查询
           Max max(Max, query(rc, l, r));
        }
         return Max;
       }

    上面的都是简单的操作,下面开始上硬菜了。

    1. 区间更新:将[l, r]区间的所有元素都更新为v。注意这里只是更新,没有查询,所以引入了“懒操作”的概念,就是不查的时候不改,查了再说,颇像赶ddl的我x。

      这里以将[l, r]区间的所有元素都更新为v为例,做法是这样的:

      1. 若当前节点的区间,被区间[l, r]覆盖,则更新并打上懒标记,表示此节点已经被更新,但是儿子还没有更新!!!然后不继续递归下去!!!

      2. 当查询的时候,如果发现这个节点有懒标记,则将懒标记下传到子节点,同时自己节点的懒标记清除,将子节点更新并做懒标记,继续查询,如果覆盖了含有懒标记的区间,就不用再递归了,只有查询区间是懒标记区间的一部分的时候,才继续下传懒标记,更新子节点。

      3. 更新操作递归回去的时候更新路径上的答案。

      板子:

       void lazy(int k, int v) {  // 更新k区间的答案,并打上懒标记
         tree[k].ans v;
         tree[k].lz v;
       }
       
       void pushdown(int k) {  // 向下传递懒标记
         lazy(lc, tree[k].lz);  // 下传给左子节点
         lazy(rc, tree[k].lz);  // 下传给右子节点
         tree[k].lz -inf;  // 清除自己的懒标记
       }
       
       void update(int k, int l, int r, int v) {  // 在k节点对应的区间,执行区间[l, r]上的更新v
         if (tree[k].>= && tree[k].<= r) {
           lazy(k, v);  // 被覆盖了,那就打上懒标记跑路~
           return;
        }
         
         // 没被覆盖
         
         // 有懒标记,则需要下放懒标记
         if(tree[k].lz != inf) {
           pushdown(k);
        }
         int mid = (tree[k].l+tree[k].r) 2;
         if (<= mid) {  // 涉及到左子树了
           update(lc, l, r, v);  // 在左子树中更新
        }
         if (mid) {  // 涉及到右子树了
           update(rc, l, r, v);  // 在右子树中更新
        }
         tree[k].ans max(tree[lc].ans, tree[rc].ans);  // 更新自己的答案
       }
    2. 区间查询(修改):

      带上区间更新之后,我们需要处理带懒标记的区间查询。

      板子:

       int query(int k, int l, int r) {  // 当前在k节点,查询[l, r]区间的答案
         if (tree[k].>= && tree[k].<= r) {  // 如果区间被覆盖,直接返回答案
           return tree[k].ans;
        }
         // 这里注意,有人可能会问如果当前区间是打上懒标记的节点的儿子怎么办,看起来儿子还没更新不是吗。
         // 注意到递归到儿子了,也就是一定经过了父亲,而经过父亲之后,必然会把懒标记下放并更新儿子,所以是没问题的。
         if (tree[k].lz != inf) {  // 有懒标记,下放!
           pushdown(k);
        }
         int mid = (tree[k].tree[k].r) 2, Max -inf;
         if (<= mid) {
           Max max(Max, query(lc, l, r));
        }
         if (mid) {
           Max max(Max, query(rc, l, r));
        }
         return Max;
       }
  4. 注意事项:

    1. 现在我们建一棵最大值为n的树,那么线段树的数组到底要开多大呢?

      注意到线段树的叶子节点数量对应最大值,都是n,也就是说已知一棵二叉树(不含度为1的节点)的叶子节点数是n,那么求一个树的总节点数的通式F,保证F尽可能接近于树的总节点数且不小于总节点数。由节点度的关系有:(F-n)x3-1+n = (F-1)*2,所以有F=2n-1。

      那是开2倍就够了吗?非也!

       

      如图所示,是n=10的情况,总节点数19,但是这个19不是连续的,所以我们最保险的情况是开到最下面一层都是满的情况,倒数第二层一定不大于n,所以最后一层不大于2n,总节点数不大于4n,所以开到4n一定没问题~

  5. 例题:

    1. (POJ3468)区间加&区间和。

       void pushdown(int k) {
         if (tree[k].lz) {  // 这里因为0的话就是不变,所以不用变成inf
           tree[lc].lz += tree[k].lz;
           tree[rc].lz += tree[k].lz;
           tree[lc].ans += (tree[lc].tree[lc].1) tree[k].lz;
           tree[rc].ans += (tree[rc].tree[rc].1) tree[k].lz;
           tree[k].lz 0;
        }
       }
       
       void update(int k, int l, int r, ll num) {  // 当前在k节点 [l, r]区间 +num
         if (tree[k].>= && tree[k].<= r) {
           tree[k].lz += num;
           tree[k].ans += num * (tree[k].tree[k].1);
           return;
        }
         pushdown(k);
         int mid = (tree[k].tree[k].r) 2;
         if (<= mid) {
           update(lc, l, r, num);
        }
         if (mid) {
           update(rc, l, r, num);
        }
         tree[k].ans tree[lc].ans tree[rc].ans;
       }
       
       ll query(int k, int l, int r) {
         if (tree[k].>= && tree[k].<= r) {
           return tree[k].ans;
        }
         pushdown(x);
         int mid = (tree[k].tree[k].r) 2;
         ll ans 0;
         if (<= mid) {
           ans += query(lc, l, r);
        }
         if (mid) {
           ans += query(rc, l, r);
        }
         return ans;
       }
    2. (HDU4902)给两种操作,一种是区间[l, r]都修改为x;另一种是对于[l, r]中大于x的数t,修改为gcd(t, x)。最后若干次操作后的序列。

      修改和gcd显然都是满足结合律的(gcd(a,b,c) = gcd(gcd(a, b), c) = gcd(a, gcd(b, c))),所以可以用线段树解决。

      搞定两个修改函数即可,懒标记表示相当于在和什么数取gcd,初始化的时候所有的叶子节点的懒标记都是自己的值,代表先和自己取了个gcd,mx是区间最大值。

       int gcd(int a, int b) {
         if (!b) return a;
         return gcd(b, a%b);
       }
       
       void pushup(int k) {
         tree[k].mx max(tree[lc].mx, tree[rc].mx);
       }
       
       void pushdown(int k) {
         if (tree[k].lz != inf) {
           tree[lc].lz tree[rc].lz tree[k].lz;
           tree[lc].mx tree[rc].mx tree[k].mx;
           tree[k].lz inf;
        }
       }
       
       void build(int k, int l, int r) {
         if (== r) {  // 相当于一个赋值
           scanf("%d", &tree[k].lz);
           tree[l] l, tree[r] r;
           tree[k].mx tree[k].lz;
           return;
        }
        int mid = (r) >> 1;
         build(l, mid, lc);
         build(mid 1, r, rc);
       }
       
       void update1(int k, int l, int r, int x) {  // 区间修改为x
         if(tree[k].>= && tree[k].<= r) {  // 被覆盖了
           // 这里不管原来的懒标记和最值,我这里修改直接覆盖
           tree[k].lz tree[k].mx x;  // 相当于和x取过了最大公约数,最大值也为x
           return;
        }
         pushdown(k);
         int mid = (tree[k].tree[k].r) 2;
         if (<= mid) update1(lc, l, r, x);
         if (mid) update1(rc, l, r, x);
         pushup(k);
       }
       
       void update2(int k, int l, int r, int x) {  // 操作2
         if (tree[k].mx <= x) return;  // 区间最大值都不超过k,这个区间显然不变
         
         // 带着懒标记(说明这个区间的值都相等) 且 被覆盖
         if (tree[k].lz != inf && tree[k].>= && tree[k].<= r) {
           // 还需要和x取gcd 相当于全变成了这个新的值
           tree[k].lz gcd(tree[k].lz, x);
           tree[k].mx tree[k].lz;
           return;
        }
         // 不然要么没覆盖要么值不都一样,不能直接return
         pushdown(k);
         int mid = (tree[k].tree[k].r) 2;
         if (<= mid) update2(lc, l, r, x);
         if (mid) update2(rc, l, r, x);
         // 最大值更新
         pushup(k);
       }
       
       void print(int k) {
         if (tree[k].== tree[k].r) {
           printf("%d ", tree[k].mx);
           return;
        }
         pushdown(k);
         int mid = (r) >> 1;
         print(lc);
         print(rc);
       }
       
       build(1, n, 1);
       // ...
       print(1);
    3. 颜色统计(POJ2777):给n个块,初始都涂成颜色1,颜色总数为T(不超过30),颜色只能是1~T,有两种操作,一种是区间涂色,一种是区间查询有多少种颜色。对于每次查询要给出答案。

      经典题目,想一想懒标记代表什么呢?显然如果能下传懒标记,应该标记区间涂了什么颜色。如果遇到了有懒标记的就返回不查了,否则就继续找下去。对于更新操作,如果覆盖,则直接更新懒标记为当前颜色,不然,将懒标记下传,递归下去进行更新。

       void build(int k, int l, int r) {
         tree[k].l;
         tree[k].r;
         tree[k].color 0;
         if (== 1) tree[k].color 1;
         if (== r) return;
         int mid = (r) >> 1;
         build(lc, l, mid);
         build(rc, mid 1, r);
       }
       
       void pushup(int k) {
         if (tree[lc].color == tree[rc].color) {
           tree[k].color tree[lc].color;
        }
       }
       
       void pushdown(int k) {
         if (tree[k].color) {
           tree[lc].color tree[rc].color tree[k].color;
           tree[k].color 0;
        }
       }
       
       void update(int k, int l, int r, int c) {
         if (tree[k].>= && tree[k].<= r) {
           tree[k].color c;
           return;
        }
         // 没覆盖所以要下传标记,然后递归查询
         pushdown(k);
         int mid = (tree[k].tree[k].r) >> 1;
         if (<= mid) {
           update(lc, l, r, c);
        }
         if (mid) {
           update(rc, l, r, c);
        }
         pushup(k);
       }
       
       void query(int k, int l, int r) {  // 最后看ans有多少个为true的, 别忘了每次query之前清空ans。
         if (tree[k].color) {
           ans[tree[k].color] true;
           return;
        }
         int mid = (tree[k].tree[k].r) >> 1;
         if (<= mid) {
           query(lc, l, r);
        }
         if (mid) {
           query(rc, l, r);
        }
       }

       

posted on 2022-05-08 22:43  小染子  阅读(356)  评论(1)    收藏  举报