点分治

点分治

1. 1 算法概述

  • 点分治,是一种针对可带权树上简单路径统计问题的算法。本质上是一种带优化的暴力,带上一点容斥的感觉。
  • 注意对于树上路径,并不要求这棵树有根,即我们只需要对无根树进行统计。接下来请把无根树这一关键点牢记于心。

1.2 问题引入

  • 给定一棵树,树上的边有权值,给定一个阈值k,请统计这棵树上总长度小于等于k的路径个数。路径长度为路径路径上所有边的权值和。POJ 1741

1.3 问题分析

  • 方法一:dfs一遍求出任一点到根的距离,枚举每一条路径u~v,通过LCA(u,v),求出路径的长度。时间效率为:\(O(n^2log(n))\)
  • 方法二:点分治。

1.4 点分治原理

  • 假设一条满足条件的路径经过点 x ,那么这条路径在 x 的一个子树里(以 x 为端点)或者在 x 的两个不同的子树里,如图:

  • 假设我们选出一个根 Root ,那么答案路径存在两种情况:

    1. 被一个子树所包含;
    2. 跨过 Root ,在黑子树中选择一部分路径,在红子树中选择一部分路径,然后从 Root 处拼起来形成一条答案路径.

    • 仔细想一下,发现情况1(被一个子树包含)中,答案路径上的一点变为根 Root ,就成了情况2(在两棵子树中)。

    • 如图, Root 为根的子树中存在答案(蓝色实边路径),可以看成以 Root2 为根的两棵子树存在答案,所以只用处理情况2就行了,可以用分治的方法,这应该是点分治的基本原理。

  • 首先根不能随便选,选根不同会影下面遍历的效率的,如图:

    • 显然选x为根比选y为根更优,选 x 最多递归2层,选 y 最多递归4层,显然可以发现找树的重心(重心所有的子树的大小都不超过整个树大小的一半)是最优的。

1.5 算法核心

  1. 对于这棵无根树,找到一个点,使得它在树的中心位置,满足如果以它为根,它的最大子树大小尽量小,这个点称为重心
  2. 以这个点为根,计算它的答案。
  3. 把以这个点为根的树的所有子树单独作为一个子问题,回到步骤1递归处理。
  • 这个算法的复杂度是多少呢?
    • 先介绍一个定理:以树的重心为根的有根树,最大子树大小不超过\(\frac{n}{2}\)。假设超过了,大小为\(k>\frac{n}{2}\),那么其他子树大小之和等于\(n−k−1\)
    • 那么把重心往这个子树方向移动,最大子树大小一定减小,那么进一步地,就证明了经过这个算法,递归的次数是\(O(logn)\) 级别。

1.6 算法实现

  • 按照上述步骤实现代码:

    1. 计算重心位置:使用一次简单的DFS来实现。
    2. 计算答案:直接用另一个DFS计算。
    3. 分治子问题:重新调用寻找重心的DFS函数,再递归求解即可。
  • 计算重心

    void GetRoot(int u,int f){
    	siz[u]=1;wt[u]=0;//siz[u]:u为根的子树节点数;wt[u]:u的节点最大的子树节点数
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(v!=f && !vis[v]){//vis[v]==1说明v是当前子树的父亲节点,如下图
    			GetRoot(v,u);//递归求子树v的重心
                siz[u]+=siz[v];//累计u的子树大小
                wt[u]=std::max(wt[u],siz[v]);//求u的最大子树
    		}
    	}//Tsiz[u]-siz[u]表示u的父亲节点除u以外其它子树之和,如下图,如果u=2,则把1子树也当做2的一个子树
    	wt[u]=std::max(wt[u],Tsiz-siz[u]);//利用的是无根树的特点
    	if(wt[Root]>wt[u])Root=u;//w[root]初始化为Inf,相当于求最大子树最小的节点u,即为重心
    }
    

    • 节点2为整个子树的重心,节点3为节点21为根的子树的重心。我们以重心3来求点分治的时候不能访问到22的其他子树。
  • 计算满足条件的答案

    void dfs(int u,int f,int d){//求以u为根的子树中其他点到u的距离+初始值d
    	a[++cnt]=d;//u到距离为d的祖先节点也是一条路径
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(v!=f && !vis[v])//vis同上图,截断子树的范围
    			dfs(v,u,d+e[i].w);
    	}
    }
    int Calc(int u,int d){
    	cnt=0;//记录u为根的子树中经过u路径条数
        dfs(u,0,d);//把经过u的路径的长度存储到a[1]~a[cnt]
    	std::sort(a+1,a+cnt+1);//从小到大排序
    	int sum=0;//计算满足条件的路径条数
    	for(int i=1,j=cnt;;++i){//双指针技巧求满足条件的组合数,比二分快
    		while(j && a[i]+a[j]>k)--j;//找到当前和a[i]组合的最大的a[j]
    		if(i>j)break;//说明找不到一个满足和a[i]组合的另一条链
    		sum+=j-i+1;//a[i]~a[j]的链都能和a[i]组合,包括a[i]单链
    	}//计算的组合包含共用同一段链的情况,如下图
    	return sum;
    }
    

    • 1为根计算路径的时候a[4]记录的1-2-4路径长度,a[5]记录的1-2-4路径的长度,他们共用了1-2这条边,点分治的核心思想,即路径要经过根节点,4~5的路径并不经过此时的根几点1,这需要我们在后面的计算中去掉。
  • 点分治核心代码

    void DFS(int u){
    	ans+=Calc(u,0);//计算u为根的子树满足条件两条路径之和小于等于k的条数(包括共边路径组合)
        vis[u]=1;//标记以u为重心的子树已计算
    	for(int i=head[u];i;i=e[i].next){
    		int v=e[i].to;
    		if(!vis[v]){//避免越界
    			ans-=Calc(v,e[i].w);//减去共边为u-v且满足条件的条数
    			Root=0;Tsiz=siz[v];//求以v为根子树的重心,Root记录子树的重心
                GetRoot(v,0);
    			DFS(Root);//子树v从重心求解满足条件的组合,是一个递归的子问题
    		}
    	}
    }
    
  • 完整代码POJ 1741

#include <cstdio>
#include <cstring>
#include <algorithm>
const int maxn = 1e4 + 5,Inf=0x3f3f3f3f;
struct Edge{int to,w,next;}e[2*maxn];
int n,k,ans,Root,Tsiz,cnt;
int head[maxn],siz[maxn],wt[maxn],a[maxn];
bool vis[maxn];
void Insert(int u,int v,int w){
	e[++head[0]].to=v;e[head[0]].w=w;e[head[0]].next=head[u];head[u]=head[0];
}
void GetRoot(int u,int f){
	siz[u]=1;wt[u]=0;//siz[u]:u为根的子树节点数;wt[u]:u的节点最大的子树节点数
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v!=f && !vis[v]){//vis[v]==1说明v是当前子树的父亲节点,如下图
			GetRoot(v,u);//递归求子树v的重心
            siz[u]+=siz[v];//累计u的子树大小
            wt[u]=std::max(wt[u],siz[v]);//求u的最大子树
		}
	}//Tsiz[u]-siz[u]表示u的父亲节点除u以外其它子树之和,如下图,如果u=2,则把1子树也当做2的一个子树
	wt[u]=std::max(wt[u],Tsiz-siz[u]);//利用的是无根树的特点
	if(wt[Root]>wt[u])Root=u;//w[root]初始化为Inf,相当于求最大子树最小的节点u,即为重心
}
void dfs(int u,int f,int d){//求以u为根的子树中其他点到u的距离+初始值d
	a[++cnt]=d;//u到距离为d的祖先节点也是一条路径
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v!=f && !vis[v])//vis同上图,截断子树的范围
			dfs(v,u,d+e[i].w);
	}
}
int Calc(int u,int d){
	cnt=0;//记录u为根的子树中经过u路径条数
    dfs(u,0,d);//把经过u的路径的长度存储到a[1]~a[cnt]
	std::sort(a+1,a+cnt+1);//从小到大排序
	int sum=0;//计算满足条件的路径条数
	for(int i=1,j=cnt;;++i){//双指针技巧求满足条件的组合数,比二分快
		while(j && a[i]+a[j]>k)--j;//找到当前和a[i]组合的最大的a[j]
		if(i>j)break;//说明找不到一个满足和a[i]组合的另一条链
		sum+=j-i+1;//a[i]~a[j]的链都能和a[i]组合,包括a[i]单链
	}//计算的组合包含共用同一段链的情况,如下图
	return sum;
}
void DFS(int u){
	ans+=Calc(u,0);//计算u为根的子树满足条件两条路径之和小于等于k的条数(包括共边路径组合)
    vis[u]=1;//标记以u为重心的子树已计算
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(!vis[v]){//避免越界
			ans-=Calc(v,e[i].w);//减去共边为u-v且满足条件的条数
			Root=0;Tsiz=siz[v];//求以v为根子树的重心,Root记录子树的重心
            GetRoot(v,0);
			DFS(Root);//子树v从重心求解满足条件的组合,是一个递归的子问题
		}
	}
}
void Solve(){
	while(~scanf("%d%d",&n,&k) && n && k){
		ans=0;memset(vis,0,sizeof(vis));
		memset(head,0,sizeof(head));
		for(int i=1;i<n;++i){
			int u,v,w;scanf("%d%d%d",&u,&v,&w);
			Insert(u,v,w);Insert(v,u,w);
		}
		wt[0]=Inf;//初始化重心所在子树节点数位无穷大,方便求重心,所以每次求子树重心前必须把root=0
        Root=0;Tsiz=n;GetRoot(1,0);//可以随便从一个节点开始求重心,这里我们从节点1开始
		DFS(Root);//从重心Root开始求满足条件的组合
		printf("%d\n",ans-n);//每个单点我们计算是都当做了一条路径,需要减去
	}
}
int main(){
	Solve();
 	return 0;
}
posted @ 2020-05-29 17:13  ♞老姚♘  阅读(426)  评论(0编辑  收藏  举报