zoukankan      html  css  js  c++  java
  • 线段树算法

    
    

    什么是线段树
    线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和,区间最大值,区间修改,单点修改等操作。
    线段树的思想和分治思想很相像。
    线段树的每一个节点都储存着一段区间[L…R]的信息,其中叶子节点L=R。它的大致思想是:将一段大区间平均地划分成2个小区间,每一个小区间都再平均分成2个更小区间……以此类推,直到每一个区间的L等于R(这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。

    线段树的原理及实现

    线段树主要是把一段大区间平均地划分成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在log级别(因为这棵线段树是平衡的)。

    下图就是一棵[1…10]的线段树的分解过程(相同颜色的节点在同一层)

    注:以下代码都是在区间求和的情况下

    储存方式

    通常用的都是堆式储存法,即编号为k的节点的左儿子编号为k∗2 k*2k∗2,右儿子编号为k∗2+1 k*2+1k∗2+1,
    通常,每一个线段树上的节点储存的都是这几个变量:区间左边界,区间右边界,区间的答案(这里为区间元素之和)
    下面是线段树的定义:

     1 //maxn表示有多少个点
     2 const int maxn=50000;
     3 struct node
     4 {
     5     //l表示左边,r表示右边;
     6     int l,r;
     7     //sum表示该线段的值
     8     int sum;
     9     //lazy为懒惰标记,能够优化区间修改的速度
    10     int lazy;
    11 }no[maxn*4];//为保证树的成功建立,节点数一般是点数的4倍以上

    初始化

    常见的做法是遍历整棵线段树,给每一个节点赋值,注意要递归到线段树的叶节点才结束。

     1 //k表示当前节点的编号,l表示当前区间的左边界,r表示当前区间的右边界
     2 void build(int k,int l,int r)
     3 {
     4     no[k].l=l;
     5     no[k].r=r;
     6     //如果递归到最低点
     7     if(l==r)
     8     {
     9         //赋值并记录该点对应的节点编号,number存放的是对应点的值
    10         no[k].sum=number[l];
    11         //pa数组存放根点对应的节点数,这样可以简化单点修改的方法
    12         pa[l]=k;
    13         return ;
    14     }
    15     //对半分
    16     int mid=(l+r)/2;
    17     //递归到左线段
    18     build(k*2,l,mid);
    19     //递归到右线段
    20     build(k*2+1,mid+1,r);
    21     //用左右线段的值更新该线段的值
    22     no[k].sum=no[k*2].sum+no[k*2+1].sum;
    23 }

    单点修改

     包含两种情况:第一种是值的加减,在这种情况下我们可以额外开一个数组pa[]用于记录每个点对应的根节点的编号,这样我们在修改时直接修改树上了个根节点的值,然后在递归回树的最高处,沿途把数据同样处理一下即可,例如addvalue()方法

    第二种情况是值的改变,就是把它的值全变成另一个值,这种情况下我们能迭代加二分判断的方法,从数的顶点开始查找,根据目标点所在树商店区间一步一步缩小范围,直到找到那个点,然后修改值并在回溯时把沿途经过的素有点的值都改一下,例如changevalue()方法

     1 //单点值加减
     2 //调用方法:addvalue(pa[i],x)
     3 //功能:从根点i对应的节点k对值进行加减,并往上调用维护树
     4 //k表示节点数的编号,x为正数表示加,x为负数表示减
     5 void addvalue(int k,int x)
     6 {
     7     no[k].sum +=x;
     8     //如果该节点不是最高的节点则往上递归
     9     if(k!=1)
    10     {
    11         addvalue(k/2,x);
    12     }
    13 }
    14 
    15 //单点值修改
    16 //调用方法:changevalue(1,x,y)
    17 //功能:从节点数1开始查找树,直到遇到根点x,并将值修改为y,并且维护树的数据
    18 //k表示节点数的编号,要把节点x的sum值变成y
    19 void changevalue(int k,int x,int y)
    20 {
    21     if(no[k].l==no[k].r)
    22     {
    23         no[k].sum=y;
    24         return ;
    25     }
    26     int mid =(no[k].l+no[k].r)/2;
    27     //判断根点x在哪个区间内
    28     //递归到左线段
    29     if(x<=mid)
    30     {
    31         changevalue(k*2,x,y);
    32     }
    33     else//递归到右线段
    34     {
    35         changevalue(k*2+1,x,y);
    36     }
    37     //用左右线段的值更新该线段的值,维护树
    38     no[k].sum=max(no[k*2].sum,no[k*2+1].sum);
    39 }

    下传标记

    下传标记时为了简化区间操作而提出的概念,正常情况下区间修改时找到所有在这个区间内的点,然后修改值并且回溯路径,但这样很容易超时,因此有一个想法是在查找树时,如果找到了这个区间的子区间时,可以先改这个子区间的值,并标记下来,而不需要再往下查找了,这样时间就会节省很多,因此标记就这么产生了,下传标记是指当你找到子区间并标记子区间时,由于在这个子区间内的所有元素都要被修改,因此不需要在查找了,只需从这个值子区间开始往下递归,改变所有的值,并删除这一层的标记将标记载往下传。大概实现就是这样子的,首先根据区间操作的不同,下传标记也分为了几类,但大体构架差不多,只是核心代码不也一样,具体情况见代码

     1 //传递标记
     2 //调用方法:pushdown(k)
     3 //功能:将节点数为k的数据往下传递
     4 //k表示节点数的编号,
     5 void pushdown(int k)
     6 {
     7     //当前节点不是最低层时
     8     if(no[k].l!=no[k].r)
     9     {
    10 
    11         //区间修改为值修改时的代码
    12         {
    13             //更新子节点的值
    14             no[k*2].sum=(no[k*2].r-no[k*2].l+1)*no[k].lazy;
    15             no[k*2+1].sum=(no[k*2+1].r-no[k*2+1].l+1)*no[k].lazy;
    16             //更新子节点的标记
    17             no[k*2].lazy=no[k*2+1].lazy=no[k].lazy;
    18         }
    19 
    20         //区间修改为值加减时的代码
    21         {
    22             //更新子节点的值
    23             no[k*2].sum+=(no[k*2].r-no[k].l+1)*no[k].lazy;
    24             no[k*2+1].sum+=(no[k*2+1].r-no[k*2+1].l+1)*no[k].lazy;
    25             //更新子节点的标记
    26             no[k*2].lazy+=no[k].lazy;
    27             no[k*2+1].lazy+=no[k].lazy;
    28         }
    29 
    30         //区间修改为值覆盖问题的代码
    31         {
    32              //更新子节点的值
    33             no[k*2].sum=no[k*2+1].sum=no[k].lazy;
    34             //更新子节点的标记
    35             no[k*2].lazy=no[k*2+1].lazy=no[k].lazy;
    36         }
    37 
    38     }
    39     //清除当前节点的标记
    40     no[k].lazy=0;
    41 }

    区间修改

    区间修改和单点修改对应也包括两种情况:第一种是区间值的加减,它的思路是在从树的定点开始往下搜索,如果遇到在目标区间内的在区间或点,将这个节点的值加上它(r-l+1)*x的值,并标记,然后再在回溯是维护一下路径就可以。

    第二种就是区间的更改,它的思路和区间值的加减相同,不同的是在遇到目标区间内的子区间或点时,这个节点的值要变为(r-l+1)*x,而不是加上,同样要表记这个节点,且回溯时一样要维护路径。

     1 //区间修改包括值修改和值加减
     2 //调用方法:sectionchange(k,l,r,x)
     3 //功能:从节点1开始查找树,修改区间[l,r]内的值,将值变为x或值加上x。并且维护线段树
     4 //k表示节点数的编号,l,r,表示目标区间,x表示要变成的值或要加减的值
     5 void sectionchange(int k,int l,int r,int x)
     6 {
     7     //检查并下传标记
     8     if(no[k].lazy)
     9     {
    10         pushdown(k);
    11     }
    12     //到对应层时更新值与标记
    13     if(no[k].l==l&&no[k].r==r)
    14     {
    15         //区间修改为值修改时的代码
    16         {
    17             no[k].sum=(no[k].r-no[k].l+1)*x;
    18             no[k].lazy=x;
    19         }
    20         //区间修改为值加减时的代码
    21         {
    22             no[k].sum+=(r-l+1)*x;
    23             no[k].lazy+=x;
    24         }
    25         return ;
    26     }
    27     //取中值
    28     int mid=(no[k].l+no[k].r)/2;
    29     if(r<=mid)
    30     {
    31         //如果被修改区间完全在左区间
    32         sectionchange(k*2,l,r,x);
    33     }
    34     else if(l>mid)
    35     {
    36         //如果被修改区间完全在右区间
    37         sectionchange(k*2+1,l,r,x);
    38     }
    39     else
    40     {
    41         //如果都不在,就要把修改区间分解成两块,分别往左右区间递归
    42         sectionchange(k*2,l,mid,x);
    43         sectionchange(k*2+1,mid+1,r,x);
    44     }
    45     //更新当前节点的值,维护线段树
    46     no[k].sum=no[k*2].sum+no[k*2+1].sum;
    47 }

    区间覆盖

    区间覆盖和区间值修改类似,但是它不适用于区间求和的问题,适合区间标记的问题,依此也要改一点代码,也能用到下传标记。具体情况将代码。

     1 //区间覆盖
     2 //调用方法:sectionchange(k,l,r,x)
     3 //功能:从节点1开始查找树,将区间[l,r]内的值都打上值为x的标记
     4 //k表示节点数的编号,l,r,表示目标区间,x表示要覆盖的值
     5 void sectioncover(int k,int l,int r,int x)
     6 {
     7     //检查并下传标记
     8     if(no[k].lazy)
     9     {
    10         pushdown(k);
    11     }
    12     //到最底层时更新值与标记
    13     if(no[k].l==l&&no[k].r==r)
    14     {
    15         no[k].sum=x;
    16         no[k].lazy=x;
    17         return ;
    18     }
    19     //取中值
    20     int mid=(no[k].l+no[k].r)/2;
    21     
    22     if(r<=mid)
    23     {
    24         //如果被修改区间完全在左区间
    25         sectioncover(k*2,l,r,x);
    26     }
    27     else if(l>mid)
    28     {
    29         //如果被修改区间完全在右区间
    30         sectioncover(k*2+1,l,r,x);
    31     }
    32     else
    33     {
    34         //如果都不在,就要把修改区间分解成两块,分别往左右区间递归
    35         sectioncover(k*2,l,mid,x);
    36         sectioncover(k*2+1,mid+1,r,x);
    37     }
    38     //更新当前节点的值,min纯属我个人需要也可以写其他的
    39     no[k].sum=min(no[k*2].sum,no[k*2+1].sum);
    40 }

    区间求和

    从树的顶点往下搜索,如果遇到目标区间内的子区间或点时,返回对应节点的值即可,过程种没有修改因此不需要维护,但如果下传标记还是要有的。

     1 //区间求和
     2 //调用方法:query(k,l,r)
     3 //功能:从节点1开始查找树,求区间[l,r]内所有数的和
     4 //k表示当前节点的编号,l表示当前区间的左边界,r表示当前区间的右边界
     5 int query(int k,int l,int r)
     6 {
     7     //如果当前区间就是询问区间,完全重合,那么显然可以直接返回
     8     if(no[k].l==l&&no[k].r==r)
     9     {
    10         return no[k].sum;
    11     }
    12     //如果当前节点被打上了懒惰标记,那么就把这个标记下传,
    13     if(no[k].lazy)
    14     {
    15         pushdown(k);
    16     }
    17     //取中值
    18     int mid = (no[k].l+no[k].r)/2;
    19     //如果询问区间包含在左子区间中
    20     if(r<=mid)
    21     {
    22         return query(k*2,l,r);
    23     }
    24     else if(l>mid)//如果询问区间包含在右子区间中
    25     {
    26         return query(k*2+1,l,r);
    27     }
    28     else//如果询问区间跨越两个子区间
    29     {
    30         return query(k*2,l,mid)+query(k*2+1,mid+1,r);
    31     }
    32 }

    区间求最值

    由于求最值的反向大概相同因此我把求最小值和最大值和到一块了,但不影响使用。首先求最值问题在初始化时就会把节点的值换成它的最值,因此它的代码没有什么难度,和区间求和的思想基本相同,不顾它返回的时最值,而且在目标区间涉及两个子区间时返回的是两个子区间的最值。

     1 //区间锥子
     2 //调用方法:querymaxin(k,l,r,falg)
     3 //功能:从节点1开始查找树,查找位于区间[l,r]中点的值的最大值和最小值,
     4 //flag用于表示最大和最小,为1时表示求最小值,为0时表示求最大值
     5 //k表示节点数的编号,l,r,表示目标区间,flag决定时求最大值还是最小值
     6 int querymaxin(int k,int l,int r,int flag)
     7 {
     8     //到对应层时返回值
     9     if(no[k].l==l&&no[k].r==r)
    10     {
    11         if(flag==1)
    12         {
    13             //返回最小
    14             return no[k].mi;
    15         }
    16         else
    17         {
    18             //返回最大
    19             return no[k].ma;
    20         }
    21     }
    22     //取中值
    23     int mid=(no[k].l+no[k].r)/2;
    24     //如果询问区间包含在左子区间中
    25     if(r<=mid)
    26     {
    27         return querymaxin(k*2,l,r,flag);
    28     }
    29     else if(l>mid)//如果询问区间包含在右子区间中
    30     {
    31         return querymaxin(k*2+1,l,r,flag);
    32     }
    33     else//如果询问区间跨越两个子区间
    34     {
    35         if(flag==1)
    36         {    //返回两个子区间的最小值
    37             return min(querymaxin(k*2,l,mid,flag),querymaxin(k*2+1,mid+1,r,flag));
    38         }
    39         else
    40         {   //返回两个子区间的最大值
    41             return max(querymaxin(k*2,l,mid,flag),querymaxin(k*2+1,mid+1,r,flag));
    42         }
    43     }
    44 }

    总结:目前我个人的题量,线段树在有下几个方面的应用:

      1:在整个数组中,通过加减或修改任意区间的值,最后求指定区间的和

      2:在一个画板中,按照顺序上色,问最后能看见几种颜色或几个颜色块

      3:给定一排相连的树,然后去掉几个树,问与第i个相连的树有几个,

  • 相关阅读:
    虚拟目录的配置
    php7.0.24-nts配置步骤
    什么是PHP
    网络篇-NSURLSessionDownloadTask上传
    网络篇-NSURLConnection原生上传
    网络篇-NSURLConnection进度下载
    网络篇-NSURLSessionDownloadTask进度下载(续上节)
    网络篇-NSURLSession介绍
    网络篇-解析XML
    多线程篇-RunLoop
  • 原文地址:https://www.cnblogs.com/mzchuan/p/11748480.html
Copyright © 2011-2022 走看看