点分治
点分治
引入:
我们对于一些树上的路径个数问题,比如距离为 \(k\) 的点对一共多少,有节点到某个节点距离为多少.....
这种题一般来说都需要 \(O(n^2)\) 的时间,但是点分治可以将其降到 \(O(n\log n)\)。
做法:
点分治实际上就是将每一个点进行对儿子的搜索,但是寻找的儿子不同。
我们知道,在一个以 \(x\) 为根的子树中,一共有两种类型的路径。
- 经过 \(x\) 点。
- 不经过 \(x\) 点。
对于前者,我们可以通过两点分别相对 \(x\) 的距离计算两者距离。
对于后者,我们就先找到这棵子树的根,然后再像前者一样求路径。
我们需要把原来的树分成很多小的子树,进行计算即可。
优化:
这时候点分治就有用了。
我们以这棵树的重心进行遍历时,其时间复杂度会降到 \(O(\log n)\) 级别。
对于每一次在子树上的计算,我们先求重心。
代码:
void find(int x,int fa){
sizes[x]=1;maxx[x]=0;//maxx表示删除节点x后产生的最大的子树的大小
//maxx[x]最小,就是树的重心。
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;//这里vis[y]防止二次搜索
find(y,x);
sizes[x]+=sizes[y];
maxx[x]=max(maxx[x],sizes[y]);
}
maxx[x]=max(maxx[x],sum-sizes[x]);//左右两边,这里的sum指的是子树的大小
if(maxx[x]<maxx[root]) root=x;
}
每次搜索都要进行遍历,有时还要结合题意进行容斥计算,大部分公式为:
void doit(int x,int z){
..........
}
void solve(int x){
vis[x]=1;ans+=doit(x,0);
for(int i=head[x];i;i=nxt[i]){
int y=ver[i],z=edge[i];
if(vis[y]) continue;
ans-=doit(y,z);//根据容斥寻找
sum=sizes[y],maxx[0]=n,root=0;
find(y,x);
solve(root);
}
}
例题:
P3806 【模板】点分治1
这题看题目名字就是模板....
将所有询问离线记录,对每个询问遍历一遍当前子树的 \(rem\) 数组,也就是记录 \(root\) 到 子节点的距离的数组。
令 \(judge[dis]\) 表示在子树中是否存在某个节点到 \(root\) 距离为 \(dis\) 。
如果 \(judge[Q[k]-rem[j]]==1\) ,则表示存在这条路径(因为同时存在一条 \(Q[k]-rem[j]\) 长度的路径)。
注意查询完之后清空,和数组的大小记录。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,inf=1e7+5e6;
int nxt[N<<1],ver[N<<1],edge[N<<1],head[N],tot;
int n,m,root,Q[N];
int S,maxx[N],sizes[N];
int vis[N],judge[inf],q[N],p,rem[N],dis[N],test[inf];
void add(int x,int y,int z){
ver[++tot]=y;edge[tot]=z;
nxt[tot]=head[x];head[x]=tot;
}
void find(int x,int fa){
sizes[x]=1;maxx[x]=0;//maxx表示删除节点x后产生的最大的子树的大小
//maxx[x]最小,就是树的重心。
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;//这里vis[y]防止二次搜索
find(y,x);
sizes[x]+=sizes[y];
maxx[x]=max(maxx[x],sizes[y]);
}
maxx[x]=max(maxx[x],S-sizes[x]);//左右两边
if(maxx[x]<maxx[root]) root=x;
}
void getdis(int x,int fa){
rem[++cnt]=dis[x];//rem求出 根 的每个子树节点到 根 的距离
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;
dis[y]=dis[x]+edge[i];
getdis(y,x);
}
}
void calc(int x){
int p=0;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(vis[y]) continue;
cnt=0;//计数用,类似于cnt
dis[y]=edge[i];
getdis(y,x);//处理x的每一个子树长度
for(int j=cnt;j;j--)//遍历当前子树
for(int k=1;k<=m;k++)
if(Q[k]>=rem[j])
test[k]|=judge[Q[k]-rem[j]];
//如果Q[k]-rem[j]的路径存在就标记第k个询问
for(int j=cnt;j;j--)//保存出现过的dis于judge;
q[++p]=rem[j],judge[rem[j]]=1;
}
for(int i=1;i<=p;i++)
judge[q[i]]=0;//处理完这个子树就清空judge
}
void solve(int x){
vis[x]=judge[0]=1;//judge[i]表示到根距离为i的路径是否存在
calc(x);//处理根为x的子树
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(vis[y]) continue;
S=sizes[y];
maxx[root=0]=inf;//S是以y为根的子树大小
find(y,0);
solve(root);//换树
}
}
int main()
{
cin>>n>>m;
for(int i=1,x,y,z;i<n;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
for(int i=1;i<=m;i++) scanf("%d",&Q[i]);
S=maxx[0]=n,root=0;
find(1,0);//寻找重心
solve(root);
for(int i=1;i<=m;++i){
if(test[i]) printf("AYE\n");
else printf("NAY\n");
}
system("pause");
return 0;
}
P2634 [国家集训队]聪聪可可
一样是模板题
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int nxt[N],ver[N],edge[N],tot,head[N];
int n,root,ans;
int dis[N],rem[N],cnt,sizes[N],son[N],maxx[N],sum,vis[N];
int res[5];
void add(int x,int y,int z){
ver[++tot]=y;edge[tot]=z;
nxt[tot]=head[x];head[x]=tot;
}
void find(int x,int fa){
sizes[x]=1,maxx[x]=0;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;
find(y,x);
sizes[x]+=sizes[y];
maxx[x]=max(sizes[y],maxx[x]);
}
maxx[x]=max(maxx[x],sum-maxx[x]);
if(maxx[x]<maxx[root]) root=x;
}
/*
我们考虑容斥:
ans=res[0]*res[0]+res[1]*res[2]*2;(组合起来有多少种)
重复统计部分:
把一颗字数里面的点距x 距离为3 的倍数的点重复统计了,所以我们还要减去子树的ans
*/
void getdis(int x,int fa){
++res[dis[x]%3];//统计
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(vis[y]||y==fa) continue;
dis[y]=(dis[x]+edge[i])%3;
getdis(y,x);
}
}
int doit(int x,int z){
memset(res,0,sizeof(res));
dis[x]=z%3; getdis(x,0);
return res[2]*res[1]*2+res[0]*res[0];
}
void solve(int x){
vis[x]=1;ans+=doit(x,0);//通过doit统计答案
for(int i=head[x];i;i=nxt[i]){
int y=ver[i],z=edge[i];
if(vis[y]) continue;
ans-=doit(y,z%3);//删除
maxx[0]=n,root=0,sum=sizes[y];
find(y,x);solve(root);
}
}
int main()
{
cin>>n;
for(int i=1,x,y,z;i<n;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
maxx[0]=sum=n;root=0;
find(1,0);
solve(root);
int k=__gcd(ans,n*n);
printf("%d/%d",ans/k,n*n/k);
system("pause");
return 0;
}
P4178 Tree
统计学
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,k;
int nxt[N],ver[N],tot,edge[N],head[N];
int maxx[N],ans;
int root,vis[N],sum,dis[N],sizes[N],rem[N],cnt;
void add(int x,int y,int z){
ver[++tot]=y;edge[tot]=z;
nxt[tot]=head[x];head[x]=tot;
}
void find(int x,int fa){
sizes[x]=1;maxx[x]=0;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;
find(y,x);
sizes[x]+=sizes[y];
maxx[x]=max(maxx[x],sizes[y]);
}
maxx[x]=max(maxx[x],sum-maxx[x]);
if(maxx[x]<maxx[root]) root=x;
}
void getdis(int x,int fa){
rem[++cnt]=dis[x];//求出子树的每个节点到根的节点
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa||vis[y]) continue;
dis[y]=dis[x]+edge[i];
getdis(y,x);
}
}
int doit(int x,int w){//利用类似于双指针的方法
cnt=0;dis[x]=w;
getdis(x,0);
sort(rem+1,rem+cnt+1);
int l=1,r=cnt,ans=0;
while(l<=r){
if(rem[l]+rem[r]<=k) ans+=r-l,++l;
else --r;
}
return ans;
}
/*
具体操作为:当遍历重心节点的每一个节点时,我们可以重新计算dis,
然后把经过了从重心到新遍历的点的边两次的路径剪掉(就是上述不合法路径)
最后统计答案即可
*/
void solve(int x){
vis[x]=1;ans+=doit(x,0);
for(int i=head[x];i;i=nxt[i]){
int y=ver[i],z=edge[i];
if(vis[y]) continue;
ans-=doit(y,z);//根据容斥寻找
sum=sizes[y],maxx[0]=n,root=0;
find(y,x);
solve(root);
}
}
int main()
{
cin>>n;
for(int i=1,x,y,z;i<n;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);add(y,x,z);
}
cin>>k;
maxx[0]=sum=n;
find(1,0);
solve(root);
cout<<ans<<endl;
system("pause");
return 0;
}
不关注的有难了😠😠😠https://b23.tv/hoXKV9