点分治

点分治

引入:

我们对于一些树上的路径个数问题,比如距离为 \(k\) 的点对一共多少,有节点到某个节点距离为多少.....

这种题一般来说都需要 \(O(n^2)\) 的时间,但是点分治可以将其降到 \(O(n\log n)\)

做法:

点分治实际上就是将每一个点进行对儿子的搜索,但是寻找的儿子不同。

我们知道,在一个以 \(x\) 为根的子树中,一共有两种类型的路径。

  1. 经过 \(x\) 点。
  2. 不经过 \(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;
}
posted @ 2021-07-01 19:40  Evitagen  阅读(98)  评论(0编辑  收藏  举报