线段树
by yyb
Type1 维护特殊信息
1.【洛谷1438】无聊的数列
维护一个数列,两种操作
1.给一段区间加上一个等差数列
2.单点询问值
维护等差数列
不难发现,等差数列可以写成(ad+b)的形式
因为具有可加性
所以维护一下这个类似于斜率的东西
每次下放的时候把数列拆分成两段,(d)值公差不变
而变化的只有后面的常数项
至于如何只在一段区间内维护等差数列
相当于在当前([l,n])位置维护这一段公差为(d)的等差数列
再在([r+1,n])维护一个负公差就行了
这题单点询问其实是简化了,完全可以维护区间询问
2.【BZOJ2243】染色
给定一棵树
两种操作
1.把一段路径染上一种颜色
2.询问路径上颜色段的个数
维护颜色段
想想怎么把颜色拼起来
假设原来两段是(111222112233)和(3332221124)
假设两段原来分别含有(k1,k2)段
拼在一起后,答案为(k1+k2)或者(k1+k2-1)
如果答案减一的话,相当于左端的最右端和右段的最左端颜色相同
所以要维护的是当前段的颜色段个数,左端点颜色和右端点颜色
3.【BZOJ1018】堵塞的交通
维护一个2×n的方格,一开始所有边都是不连通的
两个操作
1.修改一条边的连通性
2.询问两点的连通性
维护连通性
对于区间的这一段单元格
我们在意的只有四个端点点之间的连通性
所以维护六个(bool)类型
分别维护端点之间两两的连通性
当区间大小只有(1)的时候,不能够仅仅只维护连通性
还要维护边是否存在,
所以区间大小为(1)时,额外维护(4)个(bool)类型,分别表示四条边是否联通
考虑如何向上合并
既然维护了联通性,此时只需要拿出当前左右两个区间的四个点进行讨论
左端点的左上和左下,右端点的右上和右下,
分别讨论经过中间点时是否联通,从而可以合并,得到大区间四个端点的联通性
从而可以维护答案
4.【BZOJ1558】等差数列
给你一个数列,两个操作
1.给一段区间加上一个等差数列
2.将一段区间分解为最少个数的等差数列,输出等差数列的个数
线段树维护区间(dp)
可以说这道题已经非常毒瘤了
怎么考虑询问操作?
如果直接将一段数分解为等差数列?
太麻烦了。。。。
考虑相邻的数做差,
这样等差数列变为了一段连续的相等区间
考虑怎么维护分解一段区间为最少数量的等差数列
事实上,等差数列的第一项不一定要和后面的相等,所以合并的时候要额外考虑
所以,设(s[0/1/2/3])分别表示左右端点是否计算入内
同时维护最左端和最右端的值(l,r)
如果没有计算入内,则此时左右端点作为一个等差数列的开头
如果计算入内,则是一样的计算,考虑连续区间
合并的代码如下:
struct Data{int s[4],l,r;};
Data operator+(Data x,Data y)
{
Data c;c.l=x.l,c.r=y.r;
c.s[0]=x.s[2]+y.s[1]-(x.r==y.l);
c.s[0]=min(c.s[0],x.s[0]+y.s[1]);
c.s[0]=min(c.s[0],x.s[2]+y.s[0]);
c.s[1]=x.s[3]+y.s[1]-(x.r==y.l);
c.s[1]=min(c.s[1],x.s[1]+y.s[1]);
c.s[1]=min(c.s[1],x.s[3]+y.s[0]);
c.s[2]=x.s[2]+y.s[3]-(x.r==y.l);
c.s[2]=min(c.s[2],x.s[2]+y.s[2]);
c.s[2]=min(c.s[2],x.s[0]+y.s[3]);
c.s[3]=x.s[3]+y.s[3]-(x.r==y.l);
c.s[3]=min(c.s[3],x.s[3]+y.s[2]);
c.s[3]=min(c.s[3],x.s[1]+y.s[3]);
return c;
}
以(s[0])举例,(s[0])表示的是左右端点都不选
转移如下:
1.可以直接合并左边选右端点,右边选左端点。如果两者的差值相同,则可以将原来的等差数列合并为一个
2.左边两侧都不选,左边的右端点作为一个等差数列的首项,右边就要选择左端点
3.左边选右端点,右边的左端点作为一个等差数列的首项,所以右端点两边都不选
其他的(s[1/2/3])转移同理
至于区间的加法,不过是对查分数组造成两个单点修改,以及一个区间修改的影响
仔细考虑清楚就可以
总结1
这一类问题主要在于如何维护一些信息
一般的做法在于找到对应的信息的维护方式
比如对于一次函数,可以直接加和,所以维护斜率和截距
对于维护颜色段,我们发现关键在于左段的右端点和右段的左端点,所以维护左右端点
对于最大子段和,我们考虑贪心是怎么做的,所以维护最大左右子段和
对于维护连通性,我们找到联通性之间的关系,利用分类讨论来向上合并
......
总之,对于这一类维护特殊信息的问题
我们要维护的就是要求的信息,
以及向上合并时需要的信息
这一类问题的难度就在于怎么合并,
只要想清楚了怎么合并,这类问题都非常好解决
Type2 线段树维护特殊操作
1.【BZOJ3211】花神游历各国
维护一个数列,两个操作
1.区间开根
2.区间求和
很容易知道区间开根的操作次数不会很多,
(10^{12})的数据的操作次数在(6)次左右
而(sqrt 1=1,sqrt 0=0)
所以维护区间最小值(min)
对于区间开根,暴力下方,如果(min<=1)可以直接(return)
2.【BZOJ4869】相逢是问候
维护一个区间,两个操作
1.将区间[l,r]所有数变为C^(ai)
2.求区间[l,r]mod p 的和
我们根据欧拉定理
知道反复进行若干遍操作之后,答案不会再变
所以提前预处理出所有的(varphi)值
同时记录区间最少的操作次数,如果最小操作次数达到了上限,直接(return)
3.【UOJ228基础数据结构练习题】
维护一个区间,三种操作
1.区间加法
2.区间开根
3.区间求和
这是前面那题的升级版
如果再是单纯的维护区间最小值显然不合理了
我们来看看怎么开根?
如果区间所有值都相等怎么办?
显然可以直接开根
如果(max-sqrt(max)=min-sqrt(min))怎么办?
此时意味着虽然开根出来的值不同,但是减去的值相同
举个例子,比如(8,9)
开根后是(2,3)
虽然值不同,但是差相同
所以,我们把开根换成区间减法
当出现上述两种情况时下放减法标记即可
区间开根代码的实现
void Modify_Sqrt(int now,int l,int r,int L,int R)
{
if(L<=l&&r<=R)
{
ll a=sqrt(t[now].mx),b=sqrt(t[now].mn);
if(t[now].mx==t[now].mn){puttag(now,l,r,a-t[now].mx);return;}
if(t[now].mx-a==t[now].mn-b){puttag(now,l,r,a-t[now].mx);return;}
}
pushdown(now,l,r);
int mid=(l+r)>>1;
if(L<=mid)Modify_Sqrt(lson,l,mid,L,R);
if(R>mid)Modify_Sqrt(rson,mid+1,r,L,R);
t[now].v=t[lson].v+t[rson].v;
t[now].mx=max(t[lson].mx,t[rson].mx);
t[now].mn=min(t[lson].mn,t[rson].mn);
}
总结2
这类题目的重点在于这些特殊操作的处理
此时的思考的主要方向已经不是线段树如何使用了
而是想清楚当前操作具有的特殊性质
再来相应地在线段树上维护所需要的东西
Type3 作为辅助的数据结构
1.【BZOJ4552】排序
给定一个初始数列
进行若干操作
每次给定[l,r]
将这段区间进行升序或者降序的排序
最后询问第Q个位置上的数
辅助二分
显然无法直接维护排序后的结果
但是,如果是(01)序列,我们是可以直接维护排序后的结果的
那么,二分一个答案
将所有大于答案的数赋值为(1),其他的赋值为(0)
每次维护(01)序列的排序
检查目标位置是(0/1)来继续二分
2.【CF833B】The Bakery
将一个长度为n的序列分为k段,使得总价值最大
一段区间的价值表示为区间内不同数字的个数
n<=35000,k<=50
辅助(dp)
一个很简单的暴力(dp)
设(f[i][j])表示前(i)个数分为(j)段的最大总价值
转移很简单(f[i][j]=max(f[k][j-1]+Calc(k+1,i)))
其中(Calc)是题目给定的价值
但是这样复杂度显然是假的
我们重新看看这个转移
如果我们按照第二维来枚举,
那么,相当于从头开始插入每一个数
方程不变,还是(f[i][j]=max(f[k][j-1]+Calc(k+1,i)))
考虑如何计算(Calc)
其实,每一次都相当于把当前(i)位置,以及这个数上一次出现的位置(lst)之间的转移值全部加一了
所以,在线段树上面维护区间加法
同时,每次增加分段的数量的时候,重构线段树
节点的值就是上一维的(dp)值
这样就可以利用线段树来优化(进行)(dp)啦
3.【CF903G】Yet Another Maxflow Problem
一张图分为两部分,左右都有n个节点,
Ai->Ai+1连边,Bi->Bi+1连边,容量给出
有m对Ai->Bj有边,容量给出
两种操作
1.修改某条Ai->Ai+1的边的容量
2.询问从A1到Bn的最大流
n,m<=100000,流量<=10^9
这算辅助什么???简易版本网络流???
将最大流的询问转换为最小割
假设(A)侧割掉了(A_i->A_{i+1}),(B)侧割掉了(B_j->B_{j+1})
那么,(Ans=A_iA_{i+1}+B_j+B_{j+1}+A_xB_y(x<=i,y>j))
所以,对于(A)侧,我们如果枚举割掉哪一条边
我们在(B)侧都要找到对应的(j)使得答案最小
同样的,因为改变的只有(A)侧的流量,因此无论怎么修改,在(B)侧选择的(j)是不会变化的
这个时候我们就比较明朗了
现在的问题,考虑如何求解对于每个(i),最优的(B)
我们只需要扫一遍(A),一边在(B)中插入对应的值,求出区间最小值就行了
然后,把剩下的所有的值加上(Ai)后重构一棵线段树
修改就是单点修改
每次询问相当于查找全局最小值
4.【POJ1151】Atlantis
平面内有若干矩形
求面积和
辅助扫描线
这是扫描线的模板题
当然,重点不在扫描线,在于线段树
所以这里只是大致提一下
(当然啦,扫描线也要去学一下啦)
总结3
线段树不仅仅可以出裸题(虽然就算出裸题我也不一定会做)
可是可以和各种各样的东西结合起来的
然后?然后我就不认识它是线段树了
这种类型的题目不应该从线段树入手了
而是从其他算法入手,发现可以使用线段树来进行优化
这个时候才可以美滋滋的用上线段树啦
Type4 线段树一些很interesting的食用方法
1.【BZOJ3531】旅行
给出一棵树,每个节点都有一个颜色和一个权值
4个操作:
1.修改单点颜色
2.修改单点权值
3.询问路径上某种颜色的权值和
4.询问路径上某种颜色的权值最大值
颜色数、节点数、询问数<=100000
线段树动态开点
看题目的意思,我们显然要对于每种颜色维护一棵线段树
当时显然是开不下的
那么,我们考虑动态开点
发现每棵线段树显然是不满的
所以,对于每个线段树的节点,额外存下左右节点
在修改的时候,如果访问到的当前节点为空
则直接新建一个节点,否则继续访问就行了
唯一的缺点的是,如果一个点上的权值被删除,这个点并不能够回收
但是这不影响我们对于动态开点的需求
动态开点修改代码
void Modify(int &now,int l,int r,int pos,int w)
{
if(!now)now=++tot;
if(l==r){t[now].v=t[now].ma=w;return;}
int mid=(l+r)>>1;
if(pos<=mid)Modify(t[now].ls,l,mid,pos,w);
else Modify(t[now].rs,mid+1,r,pos,w);
t[now].v=t[t[now].ls].v+t[t[now].rs].v;
t[now].ma=max(t[t[now].ls].ma,t[t[now].rs].ma);
}
2.【BZOJ4653】区间
给的是UOJ的链接,UOJ的Hack数据比较强,建议在UOJ提交
从n个线段中选择M个线段
使得他们至少都包含一个相同的位置
定义一个方案的花费是M个线段的长度的最大值减最小值
求最小花费,如果无解输出-1
N<=500000,M<=200000,0≤li≤ri≤10^9
标记永久化
贪心的想一想,把区间按照长度排序,依次加入到区间中
如果把当前的线段插入进去后
这个区间的最大覆盖值超过了(M)
就把所有不需要的线段全部弹掉
然后计算贡献。
这里需要做的就是反复的区间加法
所以我们没有必要每次都把标记下放
可以直接把标记打在这个点上面
每次访问到这个点的时候直接加上这个标记值然后向上更新就好啦
代码复杂度和常数一下就降下去了了
3.【CF817F】MEX Queries
维护一个01串,一开始全部都是0
3种操作
1.把一个区间都变为1
2.把一个区间都变为0
3.把一个区间的所有数字翻转过来
每次操作完成之后询问区间最小的0的位置
l,r<=10^18
线段树上二分
这题不离散一下空间卡得我一愣一愣的
先不考虑空间的问题,直接用线段树维护一下,
放区间覆盖标记和区间翻转标记,之前已经讲过这两个标记要怎么放,不再提了
其实可以直接动态开点,每次最多产生点(60)个左右(然而事实上是(120)个)
但是如果直接这么打了发现您就(MLE/RE)了
因为产生的点其实最多是(120)个,因为在操作过程中需要下放标记
如果左右子树不存在的话必须新建,导致空间多了一倍
再加上要开(long long),发现空间开不下
所以要离散化,首先(1)要在离散数组里面,然后所有的(l,r,l+1,r+1)也必须在里面(为什么?自己思考一下)
最后是如何计算答案,
在线段树上面二分
如果左子树不满,那么答案在左子树,否则答案在右子树
如果当前这个子树都不存在,当然直接返回最左端就行了
4.【BZOJ2957】楼房重建
给定数轴上的1~n位置
每次单点修改一个位置的高度
每次修改完之后询问从原点能够看到几个位置
这应该算什么?用整棵子树来更新当前位置的神奇操作???
对于整个区间维护最大斜率以及只考虑这个区间的答案
考虑如何向上合并。
首先左半段的答案是一定存在的
所以,现在的问题就是右半段能够贡献的答案
如果右半段的最大斜率小于左半段的最大斜率,则不存在贡献
否则,如果右半段分为右左和右右两段
如果右左的最大值大于了左半段的斜率,直接加上右右段的贡献
然后递归除了右左段
否则,直接递归处理右右段
直接说有点说不清,这题需要自己好好思考一下
5.【BZOJ2733】永无乡
题目有点小复杂,直接放题面啦
永无乡包含 n座岛,编号从 1 到 n ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 n 座岛排名,名次用 1 到 n 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 a 出发经过若干座(含 0 座)桥可以 到达岛 b ,则称岛 a 和岛 b 是连通的。
现在有两种操作:
B x y 表示在岛 x 与岛 y 之间修建一座新桥。
Q x k 表示询问当前与岛 x 连通的所有岛中第 k 重要的是哪座岛,即所有与岛 x 连通的岛中重要度排名第 k 小的岛是哪座,请你输出那个岛的编号。
线段树合并
线段树合并是一个很有趣的姿势
前置技能:动态开点线段树
具体实现:每次合并两棵线段树的时候,假设叫做(t1,t2),其中要把(t2)合并进(t1)中
假设当前位置(t1)没有节点,则直接把(t2)的这个位置给(t1)(直接接上去就好啦)
如果(t2)这个位置没有节点,那么直接(return)
否则,两个位置都有节点,把两个节点的信息合并,然后递归合并左右子树
简单的代码如下:
void MergeNode(int &r1,int r2)
{
if(!r1){r1=r2;return;}
if(!r2)return;
t[r1].v+=t[r2].v;
MergeNode(t[r1].ls,t[r2].ls);
MergeNode(t[r1].rs,t[r2].rs);
}
回到这道题目
对于每一个联通快维护一个值域线段树
每次在线段树上二分一下第(K)大就好了
每次修桥相当于合并两棵线段树
用并查集维护一下联通快就可以啦,多简单
总结4
这些食用方法都很有用
包括但不限于:卡常、卡空间、降低编程困难度 等等等
而具体怎么用?
那就要灵活的看题目而定了