各种分治总结
各种分治总结
前言
分治还是实用的,但教练把点分治和 cdq 放一起也是神秘了。
点分治
为表述方便,下面定义:
- 根链=以根为起点的链
- 什么(的)子树=以什么为根的子树
算法思想
这个板子不是很简单啊,提前说一下作者的变量名起的比较随意。
link 这个题要求是否存在距离为 \(k\) 的点对。
那么显然是有树上差分做法的,但慢如屎,不介绍。
考虑到可以将路径按经不经过根划分为两类,其中经过根的可以用两个根链拼成(等下会说细节),而不经过的可以递归处理。
那为了递归时复杂度尽可能低,我们对于每个递归到的子树都把其重心\(^1\)当成根后处理即可。
- 重心定义为一颗树中的一颗节点使其最大儿子子树最小
详细流程
比如说这样一颗树:

我们注意到他十分的丑陋,因为他的左边无比强壮,但右边却相当虚弱,这导致他根的最大儿子子树\(^2\)大小为12但当我们把根换为3:
- 因为子树越大所需的递归次数越多,一个点被便利的次数越多,复杂度越大,所以这个值是影响复杂度的关键之处。

那么此时根的最大儿子子树大小就为6,显然是优了很多,这就是为什么每次都要把重心当成根的原因。
于是,我们需要一个找重心的 dfs:
void grt(int x,int f){//get root
wet[x]=0;//weight的缩写,wet[i]表示i的最大儿子子树大小,为方便写代码,wet[0]=n
siz[x]=1;//siz[i]表示i的子树大小
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v==f||vis[v]) continue;
grt(v,x);
siz[x]+=siz[v];
wet[x]=max(wet[x],siz[v]);
}
//all是目前递归的整棵树的大小
wet[x]=max(wet[x],all-siz[x]);//若这个节点为根,那么最后一个儿子子树是除自己子树外的所有点
if(wet[x]<wet[rt]) rt=x;//root的缩写,表示找到的重心
return;
}
那么现在重心找完了,我们要怎么处理经过根的路径呢,刚刚思路说了可以用两个根链拼成,但可能会有问题:

对,这就是问题,但我们发现只要是终点是不同儿子子树的根链就不会有这个问题,而反之则绝对会有这个问题,于是我们就可以通过一下方法规避掉这种情况:
void gds(int x,int f){//get dis,用于预处理一个节点到根的距离
st[++num]=dis[x];//st 是栈,等等会用
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v]||v==f) continue;
dis[v]=dis[x]+e[i].w;//处理dis
gds(v,x);
}
return;
}
void solve(int x){//用于处理以x为根的子树的子问题
int cnt=0;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v]){
continue;
}
num=0;//清空栈
dis[v]=e[i].w;//初始化dis
gds(v,x);
//先更新答案再清空是为了防止两条链终点在一个儿子子树中
for(int j=1;j<=num;j++){//用当前的儿子子树中的链更新答案
for(int k=1;k<=m;k++){
if(que[k]>=st[j]){//防止RE
ans[k]|=tong[que[k]-st[j]];
}
}
}
for(int j=1;j<=num;j++){
if(st[j]>10000000){//防止越界,询问只到这么大
continue;
}
mt[++cnt]=st[j];//mt是另一个栈,叫这个名字是与st相对,用于清空
tong[st[j]]=1;
}
}
for(int i=1;i<=cnt;i++){//这里memset就n方了
tong[mt[i]]=0;
}
return;
}
At last,我们需要一个用于递归的 dfs。
void gsz(int x,int f){//用于重新求siz
siz[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v==f||vis[v]) continue;
gsz(v,x);
siz[x]+=siz[v];
}
return;
}
void dfs(int x){
vis[x]=tong[0]=1;//把根删掉+长度为0的链存在
solve(x);//求解当前子树的答案
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v]) continue;
wet[0]=n;//这样当rt为0时任何点都能更新它
rt=0;
all=siz[v];//设置all
grt(v,0);//找儿子子树的重心
gsz(rt,0);//如果不重新找siz下一轮的all就是错的
dfs(rt);//继续递归
}
return;
}
注意这个:gsz(rt,0);//如果不重新找siz下一轮的all就是错的,但其实不影响复杂度,属于一种奇技淫巧吧,具体可见,我重置是害怕有的题要用 \(siz\) 或 \(all\) 然后被板子背刺。
哦有的人可能会想要一个完整的代码。
来吧看几道例题:
[bzoj1468] Tree
题目大意
原题晰。
题解
相对于板子只改了一点啊,对于所有的根链按长度排序一下双指针扫即可。
那有人就要问了,你排完序后遇到这种情况:

你不炸了吗!
诶,我一容斥,不就赢了吗!然后就解决了。
[国家集训队] 聪聪可可
题目大意
求在一颗树上随机取两次点,它们的最短路径长度是3倍数的概率。
题解
版子,把所有边都找到算出总可能数即可,注意取两次意味着它们可以是一个点,所以答案要 \(+n\)
[IOI 2011] Race
题目大意
求树上边数最少的长为 \(k\) 的路径。
题解
还是板子,注意到和我们最开始的板子只加了一个求最少边数,遂考虑把原本记录是否存在的桶改为记录最短边数,不存在就设为 \(inf\),然后加一个 \(dim_i\) 表示到 \(i\) 的边数即可。
[USACO13OPEN] Yin and Yang G
题目大意
原题晰。
题解
首先有典中典之把黑白转化为权值为 \(1\) 和 \(-1\) 的点。
注意到:记 \(d_i\) 表示 \(i\) 到重的权值和,对于答案有贡献的点 \(x,y\) 有以下性质:
- \(d_x+d_y=0\)
- 有点 \(z\) 满足以下两者之一:①其为 \(x\) 祖先且 \(d_z=d_x\) ②其为 \(y\) 祖先且 \(d_z=d_y\)
于是把点按有无祖先的 \(d\) 值与自己相同分类后简单维护维护就行了。
[BJOI2017] 树的难题
题目大意
原题晰。
题解
记 \(c_i\) 表示从根到 \(i\) 路径的第一条边的颜色。
然后如果两个点 \(x,y\) 满足 \(c_x=c_y\),那么更新贡献时就要减 \(c_x\) 的权值,反之不用。
于是把相同的放一块,不同的放一块算贡献,用单调队列维护,实际实现有点💩。
点分树
就是按点分治的 \(vis\) 标记顺序重构树,它有以下性质:
- 深度为 \(\log_n\)
- 两节点在点分树上的 \(lca\) 必在它原树的路径上
[BZOJ3730] 震波
题目大意
原题晰。
题解
板子,对每个节点建2棵线段树,一颗维护子树到自己,一颗维护子树到父亲,下标为距离即可。
[ZJOI2015] 幻想乡战略游戏
题目大意
给定一颗树有边权点权,点权带修,找一个点使得它到所有树上的点 \(u\) 的距离乘 \(u\) 的点权之和最小,求这个最小值。
题解
注意到可以让重心在修改后动态移动,在点分树上动即可。
[HNOI2015] 开店
题目大意
给定一颗树有边权点权,求某个点到所有点权在 \([L,R]\) 中的点的距离之和,强制在线。
题解
考虑如何求某个点到所有点的距离之和,发现和上题一模一样。
然后对颜色再开一维,vector 上二分即可。
cdq分治
算法简介
是思想不是算法,思想是分治求解然后每次用左区间更新右区间。
[bzoj3262] 陌上花开
题目大意
求三维偏序。
题解
先按 \(x\) 排序,然后分治每次考虑左区间对右区间的贡献,注意到这就是二维偏序了,对左右区间分别按 \(y\) 排序,然后用树状数组维护 \(z\) 维的贡献即可。
[BalkanOI2007] Mokia 摩基亚
题目大意
单点修改,矩阵查询
题解
把矩阵拆成4个点,然后时间和 \(x,y\) 构成三维偏序。
[bzoj2716] 天使玩偶
题目大意
单点修改,查询曼哈顿距离最近的点的曼哈顿距离。
题解
时间和 \(x,y\) 构成三维偏序。
[CQOI2011] 动态逆序对
题目大意
动态逆序对。
题解
时间和 \(x,y\) 构成三维偏序。
[HEOI2016/TJOI2016] 序列
题目大意
原题晰。
题解
考虑要有贡献首先是左右一维,然后左边的那一个的最小要比右边的本身大,左边的本身要比右边的最大大,然后三维偏序。

浙公网安备 33010602011771号