点分治
点分治
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
,那么答案路径存在两种情况:- 被一个子树所包含;
- 跨过
Root
,在黑子树中选择一部分路径,在红子树中选择一部分路径,然后从Root
处拼起来形成一条答案路径.
-
仔细想一下,发现情况
1
(被一个子树包含)中,答案路径上的一点变为根Root
,就成了情况2
(在两棵子树中)。 -
如图,
Root
为根的子树中存在答案(蓝色实边路径),可以看成以Root2
为根的两棵子树存在答案,所以只用处理情况2
就行了,可以用分治的方法,这应该是点分治的基本原理。
-
首先根不能随便选,选根不同会影下面遍历的效率的,如图:
- 显然选
x
为根比选y
为根更优,选x
最多递归2
层,选y
最多递归4
层,显然可以发现找树的重心(重心所有的子树的大小都不超过整个树大小的一半)是最优的。
- 显然选
1.5 算法核心
- 对于这棵无根树,找到一个点,使得它在树的中心位置,满足如果以它为根,它的最大子树大小尽量小,这个点称为重心。
- 以这个点为根,计算它的答案。
- 把以这个点为根的树的所有子树单独作为一个子问题,回到步骤
1
递归处理。
- 这个算法的复杂度是多少呢?
- 先介绍一个定理:以树的重心为根的有根树,最大子树大小不超过\(\frac{n}{2}\)。假设超过了,大小为\(k>\frac{n}{2}\),那么其他子树大小之和等于\(n−k−1\)。
- 那么把重心往这个子树方向移动,最大子树大小一定减小,那么进一步地,就证明了经过这个算法,递归的次数是\(O(logn)\) 级别。
1.6 算法实现
-
按照上述步骤实现代码:
- 计算重心位置:使用一次简单的
DFS
来实现。 - 计算答案:直接用另一个
DFS
计算。 - 分治子问题:重新调用寻找重心的
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
为节点2
以1
为根的子树的重心。我们以重心3
来求点分治的时候不能访问到2
及2
的其他子树。
- 节点
-
计算满足条件的答案
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;
}
hzoi