See_you_soonの学习笔记之点分治
前言
不得不说,这个点分治初学的时候是真的恶心,有些题目可能还不如树形 DP 优秀,但是在一些题目中还是很方便的(毕竟树形 DP 学得不咋地)。
算法理论
点分治说白了就是在树上查询两点之间的路径距离,一般来说树上的路径分为两种,一种是经过根的路径,另一种是在根的子树内的路径。第二种路径很好想到利用分治的思想去解决,每次只需要将根转移到它子树内的一点就可以了。第一种路径可以使用类似于求树的直径的方法,把两点到根的路径分别求出来再相加就可以了,同时还要保证这两个点不在同一棵子树内(因为这样答案一定不优)。
例题讲解
P4178 Tree
要求树上的两点 \(p\) 和 \(q\) 满足其路径长度 \(\le k\) 的个数。对于处理第二种路径中 \(dis_{p}+dis_{q}\le k\) 时有两种方案:
- 一、树上直接统计:我们先认定一个点为根节点,然后对于其子树内的每一个点都计算出其与根节点的距离,然后用一个树状数组存下来,每一次查询前缀和 \(ask(k-dis_{p})\) 即为对应的 \(q\) 的个数,然后再把此时的距离加入树状数组中 \(add(dis_{p})\)。但是这样所要存储的范围是与长度有关,并且无法对其路径进行离散化,导致复杂度就会比 \(O(n\log n)\) 还要大,就只能使用平衡树等更高级的数据结构来代替树状数组维护,由于蒟蒻太菜,这些肯定是无法优化的了
- 二、双指针扫描法,把每个节点的 \(dis_{i}\) 求出来之后进行排序,用两个指针 \(l,r\) 从两头开始扫描,如果 \(dis_{l}+dis_{r}\le k\) 时,答案 \(+1\),左端点右移;否则就让右端点左移。
剩下的就是选择哪一个点为根节点的问题了,注意到这是一棵无根树,如果给定树是一条链的情况下,我们每次都需要遍历一整条链,这样复杂度就会退化到 \(O(n^2\log n)\),为了避免这种情况的发生,我们每次找树的重心为根节点,树的重心的一条性质为其所有子树大小都小于等于 \(\frac{n}{2}\),可以保证最多递归 \(O(\log n)\) 次,能保证最后复杂度为 \(O(n\log^2n)\)。然后我们每次找到重心之后先计算下以其为根时产生的贡献,然后转移到他的子树内的节点时先把节点先前的贡献删去再算他作为根节点时的贡献。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0' || c>'9'){
if(c=='-')
f=-1;
c=getchar();
}
while(c>='0' && c<='9'){
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
int n=read(),root,tot,ans,siz[N],maxroot[N],k,tmp[N],dis[N],q[N];
//siz[i]表示第i棵子树的大小,maxroot[i]表示以i为根时的最大子树的大小,root为当前无根树的根
//dis[i]表示第i节点到根节点的距离,tmp[i]表示当前路径上长度能达到的数字
//q[i]表示所有当前路径上长度能达到的数字,tot表示当前子树内的节点数
bool vis[N]; //vis[i]表示当前节点是否被遍历过
vector<pair<int,int>> edges[N];
inline void getroot(int x,int father){
siz[x]=1;
maxroot[x]=0;
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
getroot(y.first,x);
siz[x]+=siz[y.first];
maxroot[x]=max(maxroot[x],siz[y.first]);
}
maxroot[x]=max(maxroot[x],tot-siz[x]);
if(maxroot[x]<maxroot[root])
root=x;
}
inline void getdist(int x,int father){
tmp[++tmp[0]]=dis[x];
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
dis[y.first]=dis[x]+y.second;
getdist(y.first,x);
}
}
inline int clac(int x,int w){
tmp[0]=0,dis[x]=w,getdist(x,0);
sort(tmp+1,tmp+tmp[0]+1);
int l=1,r=tmp[0],ans=0;
while(l<=r){
if(tmp[l]+tmp[r]<=k)
ans+=r-l,l++;
else
r--;
}
return ans;
}
inline void solve(int x){
vis[x]=1;
ans+=clac(x,0);
for(auto y:edges[x]){
if(vis[y.first])
continue;
ans-=clac(y.first,y.second);
tot=siz[y.first];
maxroot[root=0]=1e9;
getroot(y.first,x);
solve(root);
}
}
int main(){
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
edges[u].push_back({v,w});
edges[v].push_back({u,w});
}
k=read();
tot=n;
maxroot[root=0]=1e9;
getroot(1,0);
solve(root);
printf("%d",ans);
return 0;
}
P3806 【模板】点分治
这道题需要判断是否有等于 \(k\) 的路径,我们可以离线来做,把所有 \(k\) 存储下来,每次算出路径距离的时候就把这些询问拿出来比较,我们可以在原本路径上能到达的长度上把每次能到达的长度记录下来,然后每次与之前记录下来的长度去匹配,最后换根的时候把记录下来的全清空了就行了。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e7+9;
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0' || c>'9'){
if(c=='-')
f=-1;
c=getchar();
}
while(c>='0' && c<='9'){
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
int n,m,siz[N],maxroot[N],root,dis[N],tmp[N],q[N],que[N],tot,judge[N],ans[N];
//siz[i]表示第i棵子树的大小,maxroot[i]表示以i为根时的最大子树的大小,root为当前无根树的根
//dis[i]表示第i节点到根节点的距离,tmp[i]表示当前路径上长度能达到的数字
//q[i]表示所有当前路径上长度能达到的数字,que[i]表示询问的k,tot表示当前子树内的节点数
//judge[i]表示非当前路径上长度能到达的数字,ans[i]表示第i个询问能否成立
vector<pair<int,int>> edges[N];
bool vis[N];//vis[i]表示当前节点是否被遍历过
inline void getroot(int x,int father){//找树的重心
siz[x]=1;//初始子树大小都为1
maxroot[x]=0;
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
getroot(y.first,x);
siz[x]+=siz[y.first];//统计子树大小
maxroot[x]=max(siz[y.first],maxroot[x]);//统计最大子树的大小
}
maxroot[x]=max(maxroot[x],tot-siz[x]);//向上的子树和向下的子树大小取最大值
if(maxroot[x]<maxroot[root])//找最大子树最小的节点几位树的重心
root=x;
}
inline void getdist(int x,int father){//统计路径长度
tmp[++tmp[0]]=dis[x];//把当前路径上能到达长度加入
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
dis[y.first]=dis[x]+y.second;//求路径长度
getdist(y.first,x);
}
}
inline void clac(int x){//计算以x为根节点时的答案
int p=0;
for(auto y:edges[x]){
if(vis[y.first])
continue;
tmp[0]=0;
dis[y.first]=y.second;
getdist(y.first,x);
for(int i=tmp[0];i;i--)//枚举能到达的长度
for(int j=1;j<=m;j++)//枚举询问
if(que[j]>=tmp[i])//如果k>=能到达的长度,且存在k-这个长度的长度
ans[j]|=judge[que[j]-tmp[i]];//答案变为1
for(int i=tmp[0];i;i--)//把这些长度加入
q[++p]=tmp[i],judge[tmp[i]]=1;
}
for(int i=p;i;i--)//清空这些长度
judge[q[i]]=0;
}
inline void solve(int x){
vis[x]=judge[0]=1;//标记以x为根的情况
clac(x);//计算答案
for(auto y:edges[x]){
if(vis[y.first])
continue;
tot=siz[y.first];//tot为当前子树的长度
maxroot[root=0]=1e9;
getroot(y.first,0);
solve(root);
}
}
int main(){
//freopen("text.in","r",stdin);
//freopen("text.ans","w",stdout);
n=read(),m=read();
for(int i=1;i<n;i++){
int u=read(),v=read(),w=read();
edges[u].push_back({v,w});
edges[v].push_back({u,w});
}
for(int i=1;i<=m;i++)
que[i]=read();
maxroot[root=0]=1e9;
tot=n;
getroot(1,0);
solve(root);
for(int i=1;i<=m;i++){
if(ans[i]) puts("AYE");
else puts("NAY");
}
return 0;
}
P4149 [IOI 2011] Race
这道题其实是上面那道题的加强版,除了要满足路径长度等于 \(k\),还得要求中间经过的点最少。我们在计算路径长度的同时把此时该点距离根节点的距离记录下来,每次匹配上一个路径时取距离最小的,要注意当其一个点为根节点的时候要注意其与根节点的距离为 \(0\)。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e6+5;
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0' || c>'9'){
if(c=='-')
f=-1;
c=getchar();
}
while(c>='0' && c<='9'){
x=x*10+c-'0';
c=getchar();
}
return x*f;
}
int n=read(),k=read(),tot,ans=1e9,root,cnt,siz[N],depth[N],maxroot[N],judge[N],dis[N],q[N];
bool vis[N];
pair<int,int> tmp[N];//第一个记录的是路径长度,第二个记录的是与根节点的长度
vector<pair<int,int>> edges[N];
inline void getroot(int x,int father){
siz[x]=1;
maxroot[x]=0;
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
getroot(y.first,x);
siz[x]+=siz[y.first];
maxroot[x]=max(maxroot[x],siz[y.first]);
}
maxroot[x]=max(maxroot[x],tot-siz[x]);
if(maxroot[x]<maxroot[root])
root=x;
}
inline void getdist(int x,int father){
if(dis[x]>k)//如果此时已经大于k了,就不用再计算防止RE
return;
depth[x]=depth[father]+1;
tmp[++cnt]={dis[x],depth[x]};
for(auto y:edges[x]){
if(y.first==father || vis[y.first])
continue;
dis[y.first]=dis[x]+y.second;
getdist(y.first,x);
}
}
inline void clac(int x){
int p=0;
for(auto y:edges[x]){
if(vis[y.first])
continue;
cnt=0;
dis[y.first]=y.second;
depth[x]=0;
getdist(y.first,x);
for(int i=cnt;i;i--){
if(k>tmp[i].first)
ans=min(ans,tmp[i].second+judge[k-tmp[i].first]);
else if(k==tmp[i].first)
ans=min(ans,tmp[i].second);
}
for(int i=cnt;i;i--){
q[++p]=tmp[i].first;
judge[tmp[i].first]=min(judge[tmp[i].first],tmp[i].second);
}
}
for(int i=p;i;i--)
judge[q[i]]=INT_MAX;
}
inline void solve(int x){
vis[x]=1;
clac(x);
for(auto y:edges[x]){
if(vis[y.first])
continue;
maxroot[root=0]=n;
tot=siz[y.first];
getroot(y.first,0);
solve(root);
}
}
signed main(){
for(int i=1;i<n;i++){
int u=read()+1,v=read()+1,w=read();
edges[u].push_back({v,w});
edges[v].push_back({u,w});
}
memset(judge,0x3f,sizeof(judge));
tot=maxroot[root=0]=n;
getroot(1,0);
solve(root);
if(ans<=n)
printf("%lld", ans);
else
puts("-1");
return 0;
}
小结
点分治主要是在处理一些静态的路径长度问题,可以在较短的复杂度 \(O(\log^2n)\) 内实现,对于一些用换根 DP 十分困难的时候可以轻松完成转换,当然,有些题目可能在代码复杂度上比不过树形 DP,还有一种叫树上启发式合并(DSU on Tree)的算法可以实现点分治一样的功能,对于这些算法具体使用哪一个还是就题而论,下面扩展一下树上启发式合并的用法。

浙公网安备 33010602011771号