zoukankan      html  css  js  c++  java
  • zkw线段树

    zkw线段树是zkw大神搞的自底向上线段树,以常数小,代码短著称。然而zkw大神的原ppt中描述简单,想了好长时间才想粗来。
    以下内容针对区间最小值,使用更好理解的递归方式描述。

    定义

    zkw线段树定义如下:
    1. 它是一棵满二叉树
    2. 他的叶节点是一个数
    3. 每一个非叶节点是一个数,且这个数是它的两个孩子中的较小值

    显然,zkw线段树和普通线段树类似。他的叶节点从左到右是一个数列A1..n,非叶节点存一些信息以便查询区间最小值。
    由于它是一个满二叉树,可以用堆式储存法储存。特别的,由于叶节点的个数为2的正整数幂,对于数据规模n,叶子节点的实际个数为2lgn+1没有数据的节点用填充

    性质

    1. 不难发现,一棵有tn个节点的满二叉树有tn-1个非叶子节点。所以在堆式结构中,第i个叶子节点的位置是 tree[tn-1+i] 。这是一个很重要的性质,zkw线段树的许多操作是建立在他的基础上的。
    2. 由1容易得出,对于树上(堆中)任意一个位置i,如果i是偶数,那么它是一个左孩子;否则是一个右孩子。
                  1
               2      3
            4    5  6   7
        //显然,所有奇数都是右孩子,偶数都是左孩子

    建立数据结构

    直接用静态数组建立即可,i的左右孩子分别为i*2,i*2+1

    const int maxn = 100000;
    // 最多节点数
    int tree[maxn*4]; // 足够大,防止越界
    int n;
    // 数据规模
    int tn;
    // 叶子节点个数

    我们定义一些函数来方便下面的操作

    inline int twice(int a)
    {
      return a<<1;
      // 二倍
    }
    inline int half(int a)
    {
      return a>>1;
      // 一半
    }
    inline bool rightc(int a)
    {
      return a&1;
      // 性质2得出
    }
    inline void fix(int i) {
      tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
      // 求tree[i]并更新
    }
    

    建树

    zkw线段树的建树和普通线段树一样,这里不在赘述。不过由于使用了堆式储存,代码大大简化。

    int make_tree(int i)
    {
      if (i >= tn) 
        return tree[i];
      // 到达叶子节点
      return tree[i] = min(
      make_tree(twice(i)),
      make_tree(twice(i)+1));
      // 否则为左右孩子最小值
    }

    其中,叶节点即 tree[tn]及以后内容已经被输入,因此直接返回即可。
    非递归方式也很简单,递推即可。

    void make_tree()
    {
      for (int i=tn-1; i>=1; i--) 
        fix(i);
    }

    复杂度显然为Θ(n)。

    查询区间最小值

    这就是zkw线段树的精华所在。zkw线段树的更新和查询都是 自底向上 的。他的查询i..j最小值递归方法如下:
    1. 找到i, j的实际位置(在调用时处理),为i = tn-1+i, j = tn-1+j
    2. 如果j=i,原区间最小值为tree[i];如果j-i==1,原区间最小值为min(tree[i],tree[j])
    3. 如果i 是右孩子 ,则原区间最小值为tree[i]和区间i+1..j最小值中的较小值;如果j 是左孩子 ,则原区间最小值是tree[j]和区间i..j-1最小值中的较小值。
    4. 否则原区间最小值为区间i/2..j/2的最小值

    这种方法的正确性是显然的,只要自己动手试一试就可以明白。2是边界,3是fix;4则运用了zkw线段树的性质——i/2包含了i,i+1两个子树中的最小值,这意味着i+1这个子树不必再进行计算,只需要直接使用i/2的值。
    这是一个良好的 尾递归 算法,这意味着即使你不将递归改为循环,编译器也会自动优化他从而使他的效率可以与循环媲美。我们在最后给出循环版本的算法描述。

    /*
    * 查询i..j的区间应该调用
    * ask(tn-1+i, tn-1+j)
    */
    int ask (int i, int j)
    {
      if (i == j)
        return tree[i];
      // 只包含一个元素
      if (j-i == 1)
        return min(tree[i],tree[j]);
      // case 2
      if (rightc(i))
        return min(tree[i],ask(i+1,j));
      if (!rightc(j))
        return min(tree[j],ask(i,j-1));
      // case 3
      return ask(half(i),half(j));
      // case 4
    }

    分析算法复杂度
    不妨将操作3称为“平移”(左右两边算一次操作),操作4称为“上升”。显然平移操作越多,算法运行的越慢。然而 不可能连续进行两次平移操作 ,所以最多只有一半的操作是平移。
    再分析上升。显然的是,一次上升后,区间大小j-i会变成 j/2-i/2 = (j-i)/2 ,即区间缩小一倍。
    - 假设共进行一半平移操作。
    既然这样,就可以把一次平移和一次上升看作一个整体。n为j-i(区间大小)。列递归式:

    T(n)=T(n2)+Θ(1)

    解得算法最坏情况为O(lgn)
    - 假设没有上升,则效率最好,列递归式

    T(n)=T(n2)+Θ(1)

    解得最好情况为Ω(lgn)
    所以求区间最值复杂度为Θ(lgn)

    点修改操作

    点修改十分简单,因为第i个数简单的是 tree[tn-1+i] ,所以不再有向下试探的操作,只需要自顶向上的修改即可。
    递归形式的代码(同样是尾递归的)

    // tree[tn-1+i] = j
    // change(tn-1+i)
    void change(int i) {
      if (i == 1) return;
      fix(half(i));
      change(half(i));
    }

    循环也不难得出

    void change(int i, int j) {
      i += tn-1;
      tree[i] = j;
      i = half(i);
      while (i != 1) {
        fix(i);
        i = half(i);
      }
    }

    复杂度分析
    由递归版本得出递归式

    T(n)=T(n2)+Θ(1)

    解得 T(n)=Θ(lgn)

    zkw线段树的空间优越性

    你也许会认为开一个so big的数组空间会爆,实际上恰恰相反。 zkw线段树的空间利用率高于普通线段树 。这是因为普通线段树有大量的 指针 占用空间,zkw只使用下标索引,空间大大降低。
    经测试,zkw线段树占用空间为普通线段树的一半(最大数据)。

    zkw线段树为什么是高效的

    zkw线段树自底向下是他的先天优势,这意味着他不再需要向下试探。zkw线段树算法是 一次方法 ,因此不论是递归还是循环都减少了一半的工作量。且自底向上不需要区间覆盖的几种情况, 编程复杂度 大大降低。

    完整程序

    例题为tyvj模板题 忠诚2

    代码本来可以更紧凑,但是为了保留可读性,写的并没有zkw大神的魔性
    最后Orz zkw大神

    循环版

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    using namespace std;
    const int maxn = 100000;
    
    int tree[maxn*4];
    int n;
    int tn;
    
    inline int twice(int a)
    {
      return a<<1;
    }
    inline int half(int a)
    {
      return a>>1;
    }
    inline bool rightc(int a)
    {
      return a&1;
    }
    inline void fix(int i) {
      tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
    }
    
    void make_tree()
    {
      for (int i=tn-1; i>=1; i--) fix(i);
    }
    
    int ask (int i, int j)
    {
      int ans = 100000000;
      for (i+=tn-1,j+=tn-1; j>i;i=half(i),j=half(j)) {
        if (rightc(i)) ans = min(ans,tree[i++]);
        if (!rightc(j)) ans = min(ans,tree[j--]);
      }
      if (i == j) ans = min(ans, tree[j]);
      return ans;
    }
    
    void change(int i, int j) {
      i += tn-1;
      tree[i] = j;
      i = half(i);
      while (i != 1) {
        fix(i);
        i = half(i);
      }
    }
    
    int main() {
      int m,x,y;
      int c;
      scanf ("%d%d", &n,&m);
      memset(tree,127,sizeof tree);
      tn = n*2;
      tn = 1<<(int)(log(tn)/log(2));
      for (int i = 1; i <= n; i++) 
        scanf ("%d", &tree[tn-1+i]);
      make_tree();
      for (int i = 1; i <= m; i++) {
        scanf("%d %d %d",&c,&x,&y);
        if (c == 1)
          printf("%d ",ask(x, y));
        else {
          change(x, y);
        }
      }
      return 0;
    }

    递归版

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <cstring>
    using namespace std;
    const int maxn = 100000;
    
    inline int twice(int a)
    {
      return a<<1;
    }
    inline int half(int a)
    {
      return a>>1;
    }
    inline bool rightc(int a)
    {
      return a&1;
    }
    
    int tree[maxn*4];
    int n;
    int tn;
    
    int make_tree(int i)
    {
      if (i >= tn) 
        return tree[i];
      return tree[i] = min(
      make_tree(twice(i)),
      make_tree(twice(i)+1));
    }
    
    int ask (int i, int j)
    {
      if (i == j)
        return tree[i];
      if (j-i == 1)
        return min(tree[i],tree[j]);
      if ( rightc (i))
        return min(tree[i],ask(i+1,j));
      if (! rightc (j))
        return min(tree[j],ask(i,j-1));
      return ask(half(i),half(j));
    }
    
    inline void fix(int i) {
      tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
    }
    
    void change(int i) {
      if (i == 1) return;
      fix(half(i));
      change(half(i));
    }
    
    int main() {
      int m,x,y;
      int c;
      scanf ("%d%d", &n,&m);
      memset(tree,127,sizeof tree);
      tn = n*2;
      tn = 1<<(int)(log(tn)/log(2));
      for (int i = 1; i <= n; i++) 
        scanf ("%d", &tree[tn-1+i]);
      make_tree(1);
      for (int i = 1; i <= m; i++) {
        scanf("%d %d %d",&c,&x,&y);
        if (c == 1)
          printf("%d ",ask(tn-1+x, tn-1+y));
        else {
          tree[tn-1+x] = y;
          change(tn-1+x);
        }
      }
      return 0;
    }
  • 相关阅读:
    C#虚方法
    C#构造方法--实例化类时初始化的方法
    C#抽象类与抽象方法--就是类里面定义了函数而函数里面什么都没有做的类
    C#函数重载
    C#继承
    C#中public与private与static
    FPGA按一下按键,对应端口输出单个脉冲
    MyBatis学习 之 五、MyBatis配置文件
    MyBatis学习 之 四、动态SQL语句
    MyBatis学习 之 三、SQL语句映射文件(2)增删改查、参数、缓存
  • 原文地址:https://www.cnblogs.com/ljt12138/p/6684395.html
Copyright © 2011-2022 走看看