UniGravity
2024-10-09 15:57:47
最近研究了这几个,发现它们的形式长的挺像的,所以开个坑塞在一起讲讲。
有错误可以私信指出。
upd on 2024.10.10:增加线段树分治一道例题。修改了一些错误 by @wimg6_。
upd on 2024.10.15:增加了树分治部分。同时写完了点分治相关内容,开始写点分树。
upd on 2024.10.17
首先分治的思想即将一个问题划分为多个易求解的子问题,然后合并得到答案。
对于某些算贡献的题目,常见的分治可以是这样的(以
mid=(l+r)>>1
。好了你已经学会基础的分治了.jpg
分治的思想被广泛运用,以下会讲解分治的进阶内容和思想。
CDQ 分治的重点在于答案的合并部分。
相对于一般的分治左右两部分不相干,合并时可以直接加起来。
那如果遇到右区间的答案与左区间有关时该怎么办呢?CDQ 分治的思想则是将左区间算好的答案与右区间进行合并。
还是以
CDQ 分治的思想被运用在多个方面。
为了便于理解其思想,先从简单题开始讲起。如有基础可直接跳至三维偏序部分。
典中典逆序对
给你一个序列,求逆序对个数。
考虑分治的思想,对于
假设我们分别算完了左右区间互不相关的答案,现在还差一件事:如果有两个点
运用 CDQ 的思想,先枚举左区间的每一个数,将其丢到树状数组里。此时再枚举右区间的数
更进一步:发现逆序对就是在统计
二维偏序(数点)
有
n 个数,每个数有两个属性a_i,b_i 。对于每个i 求同时满足a_j<a_i,b_j<b_i 的j 的数量。
你发现虽然有二维,但是这样子显然不是最优的:如果对其中一维排序,就能在不改变其他条件情况下直接满足一个条件的限制。
按
如果增加到三维呢?
P3810 【模板】三维偏序(陌上花开)
有
n 个数,每个数有三个属性a_i,b_i,c_i 。求同时满足a_j\le a_i,b_j\le b_i,c_j\le c_i 的数对的数量。
我会树套树!但是树套树码量常数都大,有没有什么更好的?
根据上文的启发,不难想到先按
此时我们回想起了 CDQ 分治,即将左区间的值贡献到右区间。左贡献右就说明
怎么做呢?假如左右排好序了,不难想到使用归并排序维护左右扫到的点
这样我们就处理完左区间对右区间的贡献了。复杂度是
会不会有没算到的贡献呢?由于是分治,那些不分别在左右的数对会在后续被更新。
细节上几个注意的点:
这可比树套树好写多了(偷懒没归并):
il void work(int l,int r){
if(l==r)return;
int mid=(l+r)>>1;
work(l,mid);work(mid+1,r);
sort(v+l,v+mid+1,cmp2);sort(v+mid+1,v+r+1,cmp2);
int j=l;
forto(i,mid+1,r){
while(j<=mid&&v[j].b<=v[i].b){
arrt::add(v[j].c,v[j].cnt);j++;
}
v[i].ans+=arrt::qry(v[i].c);
}
arrt::init();
}
P3157 [CQOI2011] 动态逆序对
多次删点,每次操作后查询逆序对数量。
引入分治中重要的一个东西:时间维度。
发现前面的删除操作会干扰后面的查询,此时就不能忽略时间的影响。那我们直接把时间维也考虑进去不就行了!
如果我们按时间维排序,在时间维上分治,显然有左区间能贡献到右区间,反之则不行。
记
此时三维偏序即可。
拓展:
CDQ 套 CDQ 可以实现四维偏序(还不会)。
更高维度数据结构(或是 CDQ 层层套娃)的
所以当维度上升到一定时会选用 bitset 做法。
矩形加矩形求和
二维平面。支持某个矩形全部加上一个数和查询在矩形内点的权值和。
发现由于有修改,所以不好离线。如果可以离线那么就可以直接按
如果我们把修改也一起离线下来呢?参考上题,按时间维分治。还是左区间修改贡献右区间查询。既然是求矩形的和,那么贡献可以拆开,只需考虑当前的这段。
先把左区间的所有修改丢到平面上,直接按离线做法扫描线将右边的加上答案即可。
至于其他的修改-查询关系,只需分治到左右即可。
拓展:当修改之间不独立时(如赋值)递归顺序必须满足先左然后统计答案再右。或者使用下文的其他分治方法。
整体二分其实可以看作对可能答案的分治。
一般适用于答案是位置而不是权值和,即答案较小的情况。
P3527 [POI2011] MET-Meteors
你有长度为
n 的序列a_i ,q 次操作对区间[l,r] 整体加上c ,对于每一个i 求最先哪次操作后有a_i\ge lim_i 。
如果只需要求一个
假设二分到
能把
但是很不幸,这题不止一个
如果把所有询问放在一起二分呢?
此时我们发现每次 check 的复杂度没有大变化——只需要上一个神奇的树状数组维护每个点当前的
而且此时 check 会一次性解决所有的询问。那么把这些询问分成两类:一类是答案在
于是你想到了分治。只需对询问进行分治即可。在分治时一定要注意的是复杂度必须只与
想到上文对于单个点二分的优化,我们只需要让对
怎么实现呢?对于
总结一下:
vector 好写,但是略慢。当时代码写的方法也更复杂一些,仅供参考。时间复杂度是
il void solve(rnt l,rnt r,vector<int> qry){
if(qry.empty())return;
if(l==r){
forv(i,qry.size())ans[qry[i]]=l;
return;
}
rnt mid=(l+r)>>1;
for(rnt i=mid+1;i<=min(r,q);i++)upd[i].work(-1);
rnt id,sum;
vector<int> ok,no;
forv(i,qry.size()){
id=qry[i];sum=0;
forv(j,e[id].size())sum+=st::qry(e[id][j]);
if(sum>=b[id])ok.emplace_back(id);
else no.emplace_back(id);
}
solve(l,mid,ok);
for(rnt i=mid+1;i<=min(r,q);i++)upd[i].work(1);
solve(mid+1,r,no);
}
P1527 [国家集训队] 矩阵乘法
简单题。
先讲讲区间第
那到矩形上也是一样的,用二维树状数组维护,复杂度
决策单调性优化主要用于优化 dp。
因为不是重点,不会详细讲解证明过程。主要讲较为简单的 dp,形式为
先解决
如果
四边形不等式为
满足四边形不等式即有决策单调性。
简易证法:
反证,如果a<b 但best_a>best_b 即best_b<best_a\le a<b 。
则w(best_a,a)<w(best_b,a),w(best_b,b)<w(best_a,b) 。
二者相加,有w(best_a,a)+w(best_b,b)<w(best_b,a)+w(best_a,b) ,你发现不满足交叉优于包含。于是爆了。
同时你还发现对于奇奇怪怪的
然后有了这个终于可以切入正题了。
分治法主要用于
P3515 [POI2011] Lightning Conductor
给定一个长度为
n 的序列\{a_n\} ,对于每个i\in [1,n] ,求出一个最小的非负整数p ,使得\forall j\in[1,n] ,都有a_j\le a_i+p-\sqrt{|i-j|}
正着反着做两遍即可钦定
考虑怎么找出
其思想类似于整体二分,由于有决策单调性,一段的答案是连续的,记
既然答案连续,我们只需要找出分界点
调用
简单易懂的代码:
il void work(int l,int r,int bg,int ed){
if(l>r)return;
int mid=(l+r)>>1,best=0;
double res=0;
forto(i,bg,min(ed,mid)){
if(w(i,mid)>res)res=w(i,mid),best=i;
}
ans[mid]=max(ans[mid],res);
work(l,mid-1,bg,best);
work(mid+1,r,best,ed);
}
拓展:
与整体二分相比,可以发现的是由于决策单调性的性质,答案是连续的,而无需整体二分记 vector 表示当前答案集合。
同时决策单调性分治只需考虑
对于
就是可以撤销的并查集,只需要在更新同时维护一个栈代表将哪两个点连在一起即可。
由于需要撤销所以不能使用路径压缩。代码:
namespace DSU{
int fa[N],siz[N];
vector<pii>op;
il void init(int n){
forto(i,1,n)fa[i]=i,siz[i]=1;
}
il int find(int x){
return (x==fa[x])?x:find(fa[x]);
}
il void merge(int x,int y){
x=find(x),y=find(y);
if(siz[x]<siz[y])swap(x,y);
fa[y]=x,siz[x]+=siz[y];op.eb(mp(x,y));
}
il void pop_back(){
if(op.empty())return;
int x=op.back().first,y=op.back().second;op.pop_back();
siz[x]-=siz[y],fa[y]=y;
}
}
P5787 二分图 /【模板】线段树分治
一张
n 个点的图,有m 条无向边会分别在[l_i,r_i] 时刻存在,每个时刻判断是否是一张二分图。
先考虑对于一个确定的图如何判断是否为二分图。
这里提一下扩展域并查集,把点
并查集加边是容易的,即对于某时刻加入当前插入的边。但是删除较为困难(区分撤销,撤销指的是上一次操作取消,删除是任意某个操作)。
我们能否通过某种方式把删除转化成撤销呢?
发现
此时线段树分治登场。线段树是按时间建立的。
像区间修改一样把每个边存在的时间用 vector 存到每个节点上。
然后就是最重要的操作:最后统计答案时,对于某个节点
这样的复杂度就是分治的
代码是好久以前写的,没有格式化,可以先看下一题代码。
P4219 [BJOI2014] 大融合
动态加边和查询经过某条边的路径数量。
首先加入加边时查询贡献,显然有答案等于
这启发我们使用并查集维护节点
直接按上题做法上线段树分治即可维护某时刻的森林。
但是怎么查询呢?
好像只有在边连接的
这样就做完了。
由这两题可见,线段树分治可以用来维护容易插入而删除困难的数据结构。
线段树的码量还是比之前几个要大的。代码:
int n,q;
ll ans[N];
namespace SegT{
struct Qry{
int x,y,initid;
};
vector<Qry>qry[N];
vector<pii>op[N*5];
int bg,ed;pii dat;
il void _insert(int x,int l,int r){
if(bg<=l&&r<=ed){op[x].eb(dat);return;}
int mid=(l+r)>>1;
if(bg<=mid)_insert(x<<1,l,mid);
if(mid<ed)_insert(x<<1|1,mid+1,r);
}
il void insert(int _bg,int _ed,pii _dat){
if(_bg>_ed)return;
bg=_bg,ed=_ed,dat=_dat;_insert(1,1,q);
}
il void solve(int x,int l,int r){
for(pii p:op[x])DSU::merge(p.first,p.second);
if(l==r){
for(Qry q:qry[l])ans[q.initid]=1ll*DSU::getsiz(q.x)*DSU::getsiz(q.y);
}else{
int mid=(l+r)>>1;
solve(x<<1,l,mid);solve(x<<1|1,mid+1,r);
}
forv(i,op[x].size())DSU::pop_back();
}
}
注:下面题目原还需要图上维护线性基的技巧,与主题关系较小略过不讲,例题经过笔者转化。如有需要了解的可以看P4151 [WC2011] 最大XOR和路径。
P3733 [HAOI2017] 八纵八横 二次转化版
有一个集合
S ,多次插入和删除元素。求每个操作后集合内选择任意数字能获得的最大异或和。每个元素x<2^m\ (m\le1000) 。插入和删除的操作次数较小。
首先对于一个确定的集合,使用线性基即可求得最大异或和。注意到
我们注意到普通的线性基是难以删除和撤销的,这启发我们使用整体二分。
按照上题的套路,将每个元素存在的时间段找出来,然后将区间插入线段树。
但是你发现维护时出了困难:线性基没办法撤销,如何回溯呢?
直接暴力记下线段树上每个点一开始的线性基,回溯时赋值回去即可。
但是线段树有
可是你发现赋值完后记的线性基就没用了,而由于线段树最高
时间复杂度
LB 是线性基,bst 是长度为
namespace SegT{
vector<bst>upd[N*5];
bst ans[N];
int bg,ed;bst add;
il void insert(int x,int l,int r){
if(bg<=l&&r<=ed){upd[x].eb(add);return;}
int mid=(l+r)>>1;
if(bg<=mid)insert(x<<1,l,mid);
if(mid<ed)insert(x<<1|1,mid+1,r);
}
il void insert(int _bg,int _ed,bst _add){
if(_bg>_ed)return;
bg=_bg,ed=_ed,add=_add;
insert(1,1,q);
}
il void solve(int x,int l,int r){
if(l>r)return;
LB tmp;tmp.init(base);
forv(i,upd[x].size())base.insert(upd[x][i]);
if(l==r){
ans[l]=base.getmax();
}else{
int mid=(l+r)>>1;
solve(x<<1,l,mid);solve(x<<1|1,mid+1,r);
}
base.init(tmp);
}
}
适用范围:
点分治常用于解决与点对或路径有关的问题。
P3806 【模板】点分治 1
给定一棵有
n 个点的树,询问树上距离为k 的点对是否存在。
转化为求树上是否有长度为
首先有一个最基础的分治思想,把大问题转化为小的互不相干子问题。
对于当前子树根节点
发现未经过
考虑怎么统计经过
经过
依次枚举某棵子树内的每一个点到
这是好做的,一个思路是直接 map 查询枚举到
解决了分治的问题,但是我们发现一件事情:如果分治从一条链的一个端点开始,每次扫描都是
我们发现对于一个子树,选择什么样的
连想到树的重心,重心具有很优秀的性质。那我们只需要每次找出重心当作子树的
推荐一篇讲解复杂度的文章。
可以看到,点分治可以通过重构树的结构统计路径信息。
维护点对进阶。
P4178 Tree
给定一棵有
n 个点的树,询问树上距离小于等于k 的点对数量。
我们发现这一题和上一题很像。但是变成了查询距离小于的数量。
还是求经过
在双指针扫描的时候同时还有另一个细节:
实现时需要注意每次分治的复杂度与
int n,k,rsiz[N];
ll ans=0;
vector<pii>e[N];
il void init(int x,int fa){
rsiz[x]=1;
for(pii p:e[x]){
int y=p.first;if(y==fa)continue;
init(y,x);
rsiz[x]+=rsiz[y];
}
}
int siz[N],treesiz,minx,rt;
bool vis[N];
il void findroot(int x,int fa){
siz[x]=1;int maxs=0;
for(pii p:e[x]){
int y=p.first;if(y==fa||vis[y])continue;
findroot(y,x);
siz[x]+=siz[y],maxs=max(maxs,siz[y]);
}
maxs=max(maxs,treesiz-siz[x]);
if(maxs<minx)minx=maxs,rt=x;
}
il int find(int x){
minx=0x3f3f3f3f,rt=0,treesiz=rsiz[x];
findroot(x,0);return rt;
}
int clr[N],son[N],tot=0,dep[N],cnt[N];
il void getch(int x,int fa,int d,int rt){
son[++tot]=x,clr[x]=rt,cnt[rt]++,dep[x]=d;
for(pii p:e[x]){
int y=p.first;if(vis[y]||y==fa)continue;
getch(y,x,d+p.second,rt);
}
}
il void dfs(int x){
// printf("%d\n",x);
vis[x]=1,tot=0;
for(pii p:e[x]){
int y=p.first;if(!vis[y])getch(y,x,p.second,y);
}
sort(son+1,son+tot+1,[](int x,int y){return dep[x]<dep[y];});
forto(i,1,tot)ans+=dep[son[i]]<=k;
for(int i=1,j=tot;i<=tot;i++){
cnt[clr[son[i]]]--;
while(j>0&&dep[son[j]]+dep[son[i]]>k)cnt[clr[son[j]]]--,j--;
if(j<=i)break;
ans+=j-i-cnt[clr[son[i]]];
}
forto(i,1,tot)cnt[clr[son[i]]]=0;
for(pii p:e[x]){
int y=p.first;if(vis[y])continue;
dfs(find(y));
}
}
我们发现,点分治中每次找子树重心的性质是非常优秀的,但是通过上面两道例题,我们发现点分治只能处理一次整体的询问。对于多次询问,是否有类似的方法呢?
由于原树和点分树极易混淆,下文统一记
P6329 【模板】点分树 | 震波
给定一棵树,初始有点权,有多次询问和修改:
- 将
x 点权变为v 。- 查询树上与点
x 距离在k 内的点的点权和。
点分树其实就是将点分治过程中的父节点与子树的重心连边,可以认为是对树的高度进行了压缩,其深度是
父节点与子树的重心连边的过程显然会打乱原树的父子顺序,也就是说,点分树只能处理与父子顺序关系不大的问题,例如距离。
记
首先有一个结论,
换言之,
先考虑单次查询。由于点分树树高为
考虑拆开式子: