点分治
点分治是解决树上问题的有效手段,其思想在于对于一颗树按点分治,分成若干子树,根据题目所要求的求解树上的问题。
主要步骤
下面以LuoguP3806为例讲解其一般步骤
1.寻找一颗树的重心
既然要分治肯定需要选取一个分治中心,根据证明,当选一颗树的重心为分治中心时,其处理子树的复杂度和为最小(O(nlogn))。
所以第一步先寻找重心
方法很简单 寻找一颗树里最大的儿子最小的必然是树的重心 当然也要考虑父亲方向的大小
void getroot(int x,int fa)
{
sum[x]=1;
for(int k=lnk[x];k;k=las[k])
if(nex[k]!=fa&&!flag[nex[k]])
{
getroot(nex[k],x);sum[x]+=sum[nex[k]];
if(sum[nex[k]]>Max[x])Max[x]=sum[nex[k]];
}
if(TOT-sum[x]>Max[x])Max[x]=TOT-sum[x];
if(Max[x]<MAX)root=x,MAX=Max[x];
}
2.求解相关信息
这一步所要得到的信息就需要根据各个题目不同去选择,就本题来说,因为要求树上距离为k的点对,所以不妨就求解每个分治得到的子树中各个点到重心的距离。
void getdis(int x,int fa,int D)
{
dis[++T]=D;
for(int k=lnk[x];k;k=las[k])
if(nex[k]!=fa&&!flag[nex[k]])getdis(nex[k],x,D+cost[k]);
}
3.合并信息得到答案
在上一步得到各个点到重心的距离后,由于树上的路径必然由几段路径拼接而成,所以就先计算路径全部经过重心拼接所得到的答案。(此时计算的是某条路径长度可以被得到的次数,因为后面还需要其他操作)
对本题来说方法也很简单,就是在对于子树各个点到重心的距离处理完后,枚举每个询问和其中一个距离,用指针从后往前扫出第二个距离是否存在,其对于答案的贡献就是两者的数量相乘。
void solve(int x,int y,int id)//y和id的意思可以暂时忽略 下文有
{
int t=0;T=0;getdis(x,0,y);
sort(1+dis,1+T+dis);
t=1;top[1]=dis[1];cnt[1]=1;cnt[0]=1;
for(int i=2;i<=T;i++)
if(dis[i]!=dis[i-1])top[++t]=dis[i],cnt[t]=1;else cnt[t]++;
for(int ii=1;ii<=m;ii++)
{
int k=ask[ii],tail=t;
for(int i=0;i<=t;i++)
{
if(top[i]>k/2)break;
if(!k%2&&(top[i]==k/2)){ans[ii]+=id*cnt[i]*(cnt[i]-1)/2;break;}//(对于k/2的情况特殊处理)
while(top[tail]+top[i]>k&&i<tail)tail--;
if(top[tail]+top[i]==k)ans[ii]+=id*cnt[i]*cnt[tail];
}
}
}
经过上面的处理后,其实发现所得到的的答案是有点问题的。
问题主要在于如果对于两个节点,他们来自重心的同一棵子树,那么计算他们路径长度时其实是不需要经过重心的,但在上面的计算中却被计算了,所以需要减去。
还是对于上述代码,当需要减去时id=-1且y为重心到儿子的长度 因为所要减去的路径是原有的经过重心的路 这样在getdis的时候自然就会得到原有长度
具体过程
#include<bits/stdc++.h>
#define inf 1000000000
using namespace std;
const int maxn=20005;
int TOT,T,k,tot,n,m,a,b,c,x,Max=inf,root;
int nex[maxn],las[maxn],lnk[maxn],cost[maxn];
int ask[maxn],ans[maxn],flag[maxn],mson[maxn],sum[maxn],dis[maxn],top[maxn],cnt[maxn];
int read()
{
int ch=0,x=0;while(ch=getchar(),ch<'0'||ch>'9');
while(x=x*10+ch-48,ch=getchar(),ch>='0'&&ch<='9');
return x;
}
void add(int x,int y,int z){nex[++TOT]=y;las[TOT]=lnk[x];lnk[x]=TOT;cost[TOT]=z;}
void getroot(int x,int fa)
{
sum[x]=1;mson[x]=0;
for(int k=lnk[x];k;k=las[k])
if(nex[k]!=fa&&!flag[nex[k]])
{
getroot(nex[k],x);
sum[x]+=sum[nex[k]];
if(sum[nex[k]]>mson[x])mson[x]=sum[nex[k]];
}
if(tot-sum[x]>mson[x])mson[x]=tot-sum[x];
if(mson[x]<Max)Max=mson[x],root=x;
}
void getdis(int x,int fa,int D)
{
dis[++T]=D;
for(int k=lnk[x];k;k=las[k])
if(nex[k]!=fa&&!flag[nex[k]])getdis(nex[k],x,D+cost[k]);
}
void solve(int x,int y,int id)
{
int t=0;T=0;getdis(x,0,y);
sort(1+dis,1+T+dis);
t=1;top[1]=dis[1];cnt[1]=1;cnt[0]=1;
for(int i=2;i<=T;i++)if(dis[i]!=dis[i-1])top[++t]=dis[i],cnt[t]=1;else cnt[t]++;
for(int ii=1;ii<=m;ii++)
{
int k=ask[ii],tail=t;
for(int i=0;i<=t;i++)
{
if(top[i]>k/2)break;
if(!k%2&&(top[i]==k/2)){ans[ii]+=id*cnt[i]*(cnt[i]-1)/2;break;}
while(top[tail]+top[i]>k&&i<tail)tail--;
if(top[tail]+top[i]==k)ans[ii]+=id*cnt[i]*cnt[tail];
}
}
}
void divide(int x)//分治过程
{
getroot(x,0);flag[root]=1;solve(root,0,1);//先getroot确定重心 标记并且求解答案
for(int k=lnk[root];k;k=las[k])
if(!flag[nex[k]])
{
solve(nex[k],cost[k],-1);//对于子树处理重复答案
Max=inf;tot=sum[nex[k]]>sum[root]?sum[nex[k]]-sum[root]:sum[nex[k]];
//由于需要分治子树 所以对于MAX(求解重心用)和tot都需更改
//tot更改是需要注意,由于得到sum数组过程不一定从重心开始
//所以对于子树的sum[x]>sum[root]时 因为子树在getroot时为root的祖先 tot应为全部的减去sum[root] 否则tot就应该等于sum[x]
divide(nex[k]);
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=n-1;i++)a=read(),b=read(),c=read(),add(a,b,c),add(b,a,c);
for(int i=1;i<=m;i++)ask[i]=read();
tot=n;divide(1);//tot表示当前处理的树的大小
for(int i=1;i<=m;i++)
if(ans[i]>0)puts("AYE");else puts("NAY");
return 0;
}
点分治在处理树上问题时的效率为(O(nlogn))