线段树算法


什么是线段树
线段树,是一种二叉搜索树。它将一段区间划分为若干单位区间,每一个节点都储存着一个区间。它功能强大,支持区间求和,区间最大值,区间修改,单点修改等操作。
线段树的思想和分治思想很相像。
线段树的每一个节点都储存着一段区间[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个相连的树有几个,

原文地址:https://www.cnblogs.com/mzchuan/p/11748480.html