点分治(学习笔记)

淀粉质点分治最经典最基本的应用就是在一棵树上,对具有某些限定条件的路径静态地进行统计(比如,求一棵树上,有多少个点对满足距离小于等于k;或者一棵树上,距离为k的点对是否存在)

点分治的题目中,树一般都是无根树,所以我们要自己找一个根,这个根不能rand,根的选择会很大地影响时间效率.它只有是树的重心(树的重心:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡)时,才能保证点分治的时间效率是我们希望的nlogn.

举个例子,当一棵有n个节点的树是一条链时,如果我们指定的根在链的端点,则它要递归访问到叶节点(链的另一个端点)的时间是O(n),而如果我们指定的根在链的正中间,则它要递归访问到叶节点的时间是O(logn).根据上面树的重心的定义,这条链的正中间的点就是这棵树的重心.

void get_root(int u,int father){
    size[u]=1;f[u]=0;
//size[u]记录u的所有子树的节点个数之和
//通俗地讲就是u下面有多少个点
//初始化size[u]=1,把u点自己也算进去了.
//f[u]表示以u为根时,其最大的子树中的节点个数
    for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==father||visit[v])continue;
//visit[]标记该点是否作为根节点处理过
//(在第一次找根,即找整棵树的根时用不上)
		get_root(v,u);//递归下去
		size[u]+=size[v];
//v是u的一个子节点,以v为根的树就是u的子树
		f[u]=max(f[u],size[v]);
//根据f数组的定义,这个还是很好理解的
    }
    f[u]=max(f[u],sum-size[u]);
//这里是最难理解的,它用到了容斥原理(可以不管)
//对于当前处理的节点u,它一定是从它的父节点递归来的
//sum表示当前递归到的这棵树的大小
//根据上面的代码,
//size[u]只记录了以u的子节点为根的子树大小
//sum-size[u]就表示以u的父节点为根的子树的大小
//因为这是一棵无根树,我们现在把u当做根
//则它的父节点就成了它的子节点
    if(f[u]<f[root])root=u;
//找重心,也就是找到一个root,满足f[root]最小
}

继续,对于以root为根的一棵树(假设现在我们已经找到了根root),树上的路径只有两类:

1、路径两端点在root的不同子树中,即经过根节点root

2、路径两端点在root的一棵子树中,即不经过根节点root

而根据分治的思想,对于case2,我们又可以把root的每棵子树作为子问题,递归下去处理就行了.

所以我们只要求解case1,直接结合模板题来看代码.

传送门

题意:给定一棵有n个点的树,询问树上距离为k的点对是否存在.

int n,m,tot,root,sum,cnt;
//n个点,m个询问(k),tot用于存图,root当前找到的根
//sum当前递归到的这棵树的大小,cnt不好直接解释
int query[105];//离线存下m个询问
int pd[105];//判断当前询问的结果,1为存在,0为不存在
int size[10005];
//size[i]记录i下面有多少个点
int f[10005];
//f[i]表示以i为根节点时,最大子树的大小(节点个数)
int visit[10005];
//visit[i]判断该点是否作为根节点处理过
int dis[10005];
//dis[i]表示i到根节点的距离
//(不是深度,因为树上每条边都有权值)
int have_dis[1000005];//看代码注释会懂
int pd_dis[1000005];
//pd_dis[i]表示到根距离为i的路径是否存在(相当于桶)
//桶为什么开1000005,其实就是随便开的.
int q[1000005];//辅助数组,等下代码会有注释
int head[10005],nxt[20005],to[20005],w[20005];
//这几个数组用于存图
void add(int a,int b,int c){
    nxt[++tot]=head[a];
    head[a]=tot;
    to[tot]=b;
    w[tot]=c;
}//存图
void get_root(int u,int father){
    size[u]=1;f[u]=0;
    for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==father||visit[v])continue;
		get_root(v,u);
		size[u]+=size[v];
		f[u]=max(f[u],size[v]);
    }
    f[u]=max(f[u],sum-size[u]);
    if(f[u]<f[root])root=u;
}//上面解释了
void get_dis(int u,int father){
    have_dis[++cnt]=dis[u];
//以当前节点为根时,
//它的子树中到它的第cnt个距离是dis[u],
//其实就是把它的子树中,存在的所有到它的距离都存下来
//以便于我们下面的查询操作
    for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(v==father||visit[v])continue;
		dis[v]=dis[u]+w[i];
		get_dis(v,u);
    }
}//求have_dis[]数组
void work(int u){//处理以u为根时它的子树
    int num=0;/与q数组一起起辅助作用,看下面注释
    for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(visit[v])continue;
		cnt=0;dis[v]=w[i];
//每次get_dis前cnt要记得清零重置
//因为u是根,v是根的子节点,所以可以直接dis[v]=w[i]
		get_dis(v,u);
		for(int j=1;j<=cnt;j++)//枚举出所有距离
	    	for(int k=1;k<=m;k++){//枚举询问
				if(pd[k])continue;//小优化
				if(query[k]>=have_dis[j])
		    		if(pd_dis[query[k]-have_dis[j]])pd[k]=1;
//因为我们处理的是第一种路径,即经过根节点
//所以我们枚举 其中一个点到根节点的距离have_dis[j]
//那么如果要对答案产生贡献,则
//另一个点到根节点的距离是query[k]-have_dis[j]
//然后判断这个距离是否存在,如果存在则产生了贡献.
//因为当前这棵子树对pd_dis[]产生的贡献,下面才更新
//所以pd_dis[]中的距离都是别的子树上的点到根节点的距离
//所以这种写法不用考虑去重了
	    }
		for(int j=1;j<=cnt;j++){
	    	q[++num]=have_dis[j];//看下面注释
	    	pd_dis[have_dis[j]]=1;
		}
    }
    for(int i=1;i<=num;i++)pd_dis[q[i]]=0;
//用memset清空会超时,如果直接1-1000005(数组大小)清空也会超时
//have_dis数组反正会被重新覆盖掉,没必要清空
//所以借助数组q来清空.
}
void solve(int u){
    visit[u]=1;pd_dis[0]=1;
//标记u点作为根节点处理过了
//pd_dis[0]=1表示到当前根u距离为0的点存在,即它本身
//因为可能存在根节点到某个点的距离恰好为k的情况
    work(u);//处理u的子树
    for(int i=head[u];i;i=nxt[i]){
//对u的子树分治处理
		int v=to[i];
		if(visit[v])continue;
//和第一次对整棵树处理的操作都一样
		sum=size[v];root=0;f[root]=sum;
		get_root(v,0);solve(v);
    }
}
int main(){
    n=read();m=read();
    for(int i=1;i<n;i++){
		int a=read(),b=read(),c=read();
		add(a,b,c);add(b,a,c);
    }//注意无根树的边是无向边
    for(int i=1;i<=m;i++)query[i]=read();
//把所有询问离线
    f[root]=sum=n;
//此时root是0,根据定义应该好理解.
    get_root(1,root);//找到整棵树的重心
    solve(root);//从根开始对整棵树点分治
    for(int i=1;i<=m;i++){
		if(pd[i])puts("AYE");//如果存在
		else puts("NAY");
    }
    return 0;
}

传送门

题意:给你一棵树,求有多少点对它们两者间的距离小于等于K?

分析:上题是等于k的点对是否存在?本题是小于等于k的点对的个数.总体的框架,即模板还是不变的.搞懂了上题,这题就很容易懂了.就只讲一下不同的地方了.其实只有work函数和solve函数的一点点不同.

int work(int u,int dist){
    dis[u]=dist;
    cnt=0;get_dis(u,0);
    sort(have_dis+1,have_dis+1+cnt);
    int l=1,r=cnt,Ans=0;
    while(l<r){//此时序列具有单调性
		if(have_dis[l]+have_dis[r]<=k)Ans+=r-l,l++;
		else r--;
    }
    return Ans;
}
void solve(int u){
    visit[u]=1;
    ans+=work(u,0);
//当前处理的这棵树的根节点是u
//work(u,0)计算经过根节点u的合法路径的点对个数
    for(int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(visit[v])continue;
		ans-=work(v,w[i]);//去重
//为什么要去重呢?因为上面我们ans+=work(u,0)时,
//考虑这样一种情况,就是两个点x,y在u的同一棵子树vi上
//但是dis[x]+dis[y]<=k,对于这种情况我们统计了一次
//而这种情况在以u的子节点vi为根时又会被算一次
		sum=size[v];root=0;f[root]=sum;
		get_root(v,0);solve(v);
    }
}

传送门

题意:题意:给你一棵树,求有多少点对(可以是同一个点)它们两者间的距离是3的倍数?(注意:0也是3的倍数)

我们之前两道题的have_dis数组存的是到根节点的距离,本题中我们让它存到根节点的距离mod 3后分别为0,1,2的个数.每换了一个根节点,就要清空.

对于每个根节点,它对答案的贡献都是0的个数乘0的个数+2乘1的个数乘2的个数.

为什么是这个式子呢?其实我们是在n个点中选1个点,然后又在n个点中再选一个点(因为可以是同一个点),所以总方案有(n^2)种.两次都选0的话,我们是在一个集合中选两个数,没有先后之分,直接乘法原理.一次选1,一次选2的话,我们是分别在不同的集合中选一个数,是有先后之分的,所以乘法原理之后还要乘上2.

本题卡常了,可能是码风太丑,吸了一口氧.

inline void get_dis(int u,int father){
    have_dis[dis[u]%3]++;//不同之处
    for(rg int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(visit[v]||v==father)continue;
		dis[v]=dis[u]+w[i];
		get_dis(v,u);
    }
}
inline int work(int u,int dist){
    have_dis[0]=have_dis[1]=have_dis[2]=0;
//记得初始化为0
    dis[u]=dist;get_dis(u,0);
    return have_dis[0]*have_dis[0]+2*have_dis[1]*have_dis[2];
}
inline void solve(int u){
    visit[u]=1;
    ans+=work(u,0);
    for(rg int i=head[u];i;i=nxt[i]){
		int v=to[i];
		if(visit[v])continue;
		ans-=work(v,w[i]);
		sum=size[v];root=0;f[root]=sum;
		get_root(v,0);solve(v);
    }
}
int main(){
    n=read();
    for(rg int i=1;i<n;i++){
		rg int a,b,c;
		a=read();b=read();c=read();
		add(a,b,c);add(b,a,c);
    }
    sum=f[root]=n;
    get_root(1,0);
    solve(root);
//注意一下输出格式,题目要求输出概率
    int ppx=__gcd(ans,fm);
    printf("%d/%d
",ans/ppx,fm/ppx);
    return 0;
}

原文地址:https://www.cnblogs.com/PPXppx/p/10386680.html