点分治

点分治适用于处理大规模的无修改的树上路径信息。

钦定一个点作为根节点,路径有且仅有两种情况:

  1. 经过根节点,即由两颗子树构成
  2. 不经过根节点,即只在根节点的一颗子树之中

对于情况1,直接计算即可。对于情况2,我们需要枚举其它点作为根来计算这种情况,为了让递归枚举根的层数最少,我们选择每次选择子树的重心作为根节点,此时递归层数最少。

在统计的过程中,一般情况下我们可以通过单步容斥原理,即用“从子树中任意选出两个点”的结果减去“从同一个儿子的子树中任意选出两个点”的结果,得到每次分治的结果。对于\(x\)的每一个儿子,用同样的方法便可得到正确统计结果。注意,在用单步容斥原理时需要满足一个条件,即统计结果可以逆运算,如果不存在逆运算,最典型的例子便是统计最大值和最小值,像这样的问题只能就题论题,通过特殊方法加以解决。

P3806 【模板】点分治1

询问树上距离为\(k\)的点对是否存在。

离线记录询问,设当前根为\(root\),子树为\(son1[1...t]\),对于\(son[i]\),保存\(son[i]\)中每个点到\(root\)的距离与\(tmpdis[]\)\(have[dis]\)表示子树\(son[1...i-1]\)中是否存在节点到\(root\)的距离为\(dis\),遍历每个询问和当前子树的\(tmpdis\),若\(have[询问-tmpdis]=1\),那么该点对存在。将\(tmpdis\)存入清空数组,在处理完\(root\)所有子树后清空。

void getroot(int x,int fa){
    sz[x]=1;
    ms[x]=0;/*删除x后的子树中最大的树的大小*/
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        getroot(y,x);
        sz[x]+=sz[y];
        ms[x]=max(ms[x],sz[y]);/*与x相连子树进行比较*/
    }
    ms[x]=max(ms[x],sum-sz[x]);/*与x父节点一侧进行比较*/
    if(ms[x]<ms[root])root=x;/*更新重心*/
}
void getdis(int x,int fa){/*将子树的dis加入tmpdis[]*/
    tmpdis[++tmpdis[0]]=dis[x];
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        dis[y]=dis[x]+e[i].w;
        getdis(y,x);
    }
}
void calc(int x){
    rec[0]=0;/*计算每个根是回收数组清空*/
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        tmpdis[0]=0;/*清空当前子树的tmpdis[]*/
        dis[y]=e[i].w;/*初始的dis是边权*/
        getdis(y,x);
        for(int j=tmpdis[0];j;j--)for(int k=1;k<=m;k++)if(ask[k]>=tmpdis[j])ans[k]|=have[ask[k]-tmpdis[j]];/*遍历询问和tmpdis获取答案*/
        for(int j=tmpdis[0];j;j--)rec[++rec[0]]=tmpdis[j],have[tmpdis[j]]=1;/*回收tmpdis,并更新have*/
    }
    for(int i=1;i<=rec[0];i++)have[rec[i]]=0;/*清空have*/
}
void divide(int x){
    vis[x]=have[0]=1;/*距离为0已经存在*/
    calc(x);
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        sum=sz[y];
        root=0;
        getroot(y,x);/*对y找重心*/
        divide(root);
    }
}
    ms[0]=sum=n;/*初始根为0,点数和为n*/
    getroot(1,0);
    divide(root);

P2634 [国家集训队]聪聪可可

求树上两点间距离为\(3\)的倍数的概率。

\(3\)取余后,考虑当前子树链内和之前已有的距离产生的贡献,由于是有序点对,于是贡献为原先的\(0*\)当前的\(0*2\)+原先的\(1*\)当前的\(2*2\)+原先的\(2*\)当前的\(1*2\),再加上根节点和当前子树内部产生的贡献,即当前的\(0*2\),注意没有计算两个点重合的情况,要再最后\(ans+=n\).

void getdis(int x,int fa){
    now[dis[x]%3]++;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        dis[y]=dis[x]+e[i].w;
        getdis(y,x);
    }
}
inline void calc(int x){
    buc[0]=buc[1]=buc[2]=0;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        dis[y]=e[i].w;
        now[0]=now[1]=now[2]=0;
        getdis(y,x);
        ans+=(buc[0]*now[0]+buc[1]*now[2]+buc[2]*now[1]+now[0])<<1;
        buc[0]+=now[0],buc[1]+=now[1],buc[2]+=now[2];
    }
    buc[0]=buc[1]=buc[2]=0;
}

当然也可以写容斥。

void getdis(int x,int fa){
    sm[dis[x]%3]++;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        dis[y]=dis[x]+e[i].w;
        getdis(y,x);
    }
}
inline int calc(int x){
    sm[0]=sm[1]=sm[2]=0;
    getdis(x,0);
    return (sm[1]*sm[2]<<1)+sm[0]*sm[0];
}
void divide(int x){
    vis[x]=1;
    dis[x]=0;
    ans+=calc(x);
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        dis[y]=e[i].w;
        ans-=calc(y);
        root=0;
        sum=sz[y];
        getroot(y,x);
        divide(root);
    }
}

P4149 [IOI2011]Race

求树上两点间距离为\(k\)时的最小边数。

在对\(root\)的每个子树进行计算时,保留之前的子树中的\(dis\)和对应的边数,为每一个不同的\(dis\)记录一个最小的边数,当\(ans>=n\)则无解,初始化每个\(dis\)对应的边数都要时正无穷。

void getdis(int x,int fa,int dis,int num){
    if(dis>k)return;
    p[++top]={dis,num};
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        getdis(y,x,dis+e[i].w,num+1);
    }
}
void calc(int x){
    mi[0]=top=0;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        int last=top;
        getdis(y,x,e[i].w,1);
        for(int j=last+1;j<=top;j++)ans=min(ans,mi[k-p[j].first]+p[j].second);
        for(int j=last+1;j<=top;j++)mi[p[j].first]=min(mi[p[j].first],p[j].second);
    }
    for(int i=1;i<=top;i++)mi[p[i].first]=inf;
}
    memset(mi,0x3f,sizeof(mi));

P4178 Tree

求两点间距离\(<=k\)的点对的数量。

在分治根的每棵子树时,将节点存入桶中,运用染色法,标记每个节点所属的是\(root\)的哪棵子树,维护当前子树出现的次数,将桶中的点按照\(dis\)从小到大排序,双指针扫描的同时维护每个\(col\)的出现次数。

void getdis(int x,int fa){
    col[x]=col[fa];
    cnt[col[x]]++;
    buc[++buc[0]]=x;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        dis[y]=dis[x]+e[i].w;
        getdis(y,x);
    }
}
void calc(int x){
    dis[x]=buc[0]=0;
    col[x]=x;
    cnt[col[x]]=1;
    buc[++buc[0]]=x;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        dis[y]=e[i].w;
        col[y]=y;
        cnt[col[y]]=0;
        getdis(y,y);
    }
    sort(buc+1,buc+1+buc[0],[](int x,int y){return dis[x]<dis[y];});
    int l=1,r=buc[0];
    while(l<r){
        while(l<r&&dis[buc[l]]+dis[buc[r]]<=k){
            ans+=r-l+1-cnt[col[buc[l]]];
            cnt[col[buc[l++]]]--;
        }
        cnt[col[buc[r--]]]--;
    }
}

也可以容斥处理。

void getdis(int x,int fa,int dis){
    res[++res[0]]=dis;//存入距离
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y!=fa&&!vis[y])getdis(y,x,dis+e[i].w);
    }
}
int calc(int x,int y){
    res[0]=0;getdis(x,0,y);
    sort(res+1,res+1+res[0]);
    int l=1,r=res[0],ans=0;//l和r经过根节点的距离都<=k,那么在它们之间的点对经过根节点的距离肯定更<=k
    while(l<=r)(res[l]+res[r]<=k)?(ans+=r-l,++l):(--r);
    return ans;
}
void divide(int x){
    vis[x]=1;
    ans+=calc(x,0);
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        ans-=calc(y,e[i].w);
        sum=sz[y];rt=0;
        getroot(y,0);
        divide(rt);
    }
}

采药人的药田是一个树状结构,每条路径上都种植着同种药材。
采药人以自己对药材独到的见解,对每种药材进行了分类。大致分为两类,一种是阴性的,一种是阳性的。
采药人每天都要进行采药活动。他选择的路径是很有讲究的,他认为阴阳平衡是很重要的,所以他走的一定是两种药材数目相等的路径。采药工作是很辛苦的,所以他希望他选出的路径中有一个可以作为休息站的节点(不包括起点和终点),满足起点到休息站和休息站到终点的路径也是阴阳平衡的。他想知道他一共可以选择多少种不同的路径

/*
判断一条从根出发的路径是否含有休息站:
dfs时记录下前缀和x,标记是否存在前缀和为x的节点
枚举根节点的每个子树
f[i][0/1]为之前扫过的子树的路径和为i的路径数目
g[i][0/1]位当前子树的路径和为i的路径数目
0为路径上不存在前缀和为i的节点,1为存在
当前子树贡献为f[0][0]*g[0][0]+sum(f[i][0]*g[-i][1]+f[i][1]*g[-i][0]+f[i][1]*g[-i][1])(i=-maxdep->maxdep)
*/
#include<bits/stdc++.h>

using namespace std;

#define int long long
const int N=2e5+5;
int h[N],tot,sz[N],ms[N],rt,sum,maxdep,f[N<<1][2],g[N<<1][2],dep[N],buc[N<<1],dis[N],ans,n;
bool vis[N<<1];
struct edge{
    int to,next,w;
}e[N<<1];
inline void add(int x,int y,int w){
    e[++tot]={y,h[x],w};h[x]=tot;
}
void getroot(int x,int fa){
    sz[x]=1;ms[x]=0;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        getroot(y,x);
        sz[x]+=sz[y];
        ms[x]=max(ms[x],sz[y]);
    }
    ms[x]=max(ms[x],sum-sz[x]);
    if(ms[x]<ms[rt])rt=x;
}
void calc(int x,int fa){
    maxdep=max(maxdep,dep[x]);
    buc[dis[x]+N]?f[dis[x]+N][1]++:f[dis[x]+N][0]++;
    buc[dis[x]+N]++;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(y==fa||vis[y])continue;
        dis[y]=dis[x]+e[i].w;dep[y]=dep[x]+1;
        calc(y,x);
    }
    buc[dis[x]+N]--;
}
void divide(int x){
    vis[x]=g[N][0]=1;
    int premax=0;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        maxdep=dep[y]=1;dis[y]=e[i].w;
        calc(y,x);
        premax=max(premax,maxdep);
        ans+=f[N][0]*(g[N][0]-1);
        for(int j=-maxdep;j<=maxdep;j++)ans+=f[j+N][1]*g[N-j][0]+f[j+N][0]*g[N-j][1]+f[j+N][1]*g[N-j][1];
        for(int j=-maxdep;j<=maxdep;j++)g[j+N][0]+=f[j+N][0],g[j+N][1]+=f[j+N][1],f[j+N][0]=f[j+N][1]=0;
    }
    for(int j=-premax;j<=premax;j++)g[j+N][0]=g[j+N][1]=0;
    for(int i=h[x];i;i=e[i].next){
        int y=e[i].to;
        if(vis[y])continue;
        rt=0;sum=sz[y];
        getroot(y,x);divide(rt);
    }
}
signed main(){
    cin>>n;
    for(int i=1;i<n;i++){
        int a,b,c;cin>>a>>b>>c;c=c?1:-1;
        add(a,b,c),add(b,a,c);
    }
    ms[0]=sum=n;getroot(1,0);
    divide(rt);
    cout<<ans;
    return 0;
}
posted @ 2022-11-14 17:51  半步蒟蒻  阅读(116)  评论(0)    收藏  举报