poj-1741 (点分治模板)
题目
Description
Give a tree with n vertices,each edge has a length(positive integer less than 1001).
Define dist(u,v)=The min distance between node u and v.
Give an integer k,for every pair (u,v) of vertices is called valid if and only if dist(u,v) not exceed k.
Write a program that will count how many pairs which are valid for a given tree.
Input
The input contains several test cases. The first line of each test case contains two integers n, k. (n<=10000) The following n-1 lines each contains three integers u,v,l, which means there is an edge between node u and v of length l.
The last test case is followed by two zeros.
Output
For each test case output the answer on a single line.
Sample Input
5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0
Sample Output
8
分析
-
题目大意就是给一棵树,每条边有一个长度,两个点之间的距离就是连接它俩的边的长度之和。求出这棵树中满足距离不大于 K 的点对个数。
-
这基本上就是树的点分治模板题,我也是第一次打树的分治,主要有以下几个步骤:
-
对于一棵子树的分治,首先求出它的重心, 我们目的是对这棵子树讨论所有经过重心的路径。(重心的作用是保证时间复杂度)
-
之后,以此重心为根节点,对这棵树进行一遍深搜,得出每个节点到重心的距离dis[]
-
之后,这棵子树中相连的路径会经过重心且对答案有贡献的点对(
i,j
) (i<j
)就会是这样:dis[i]+dis[j] <= K
且在去除重心后,i 与 j 不在同一个联通块里。 -
不过显然要满足“不在同一个联通块里”这个条件有点突兀,于是就有了一个小技巧:
-
先不管在不在一个联通块这个条件,算出当前这棵树的符合路径数,之后再将得出的个数减去 以重心的儿子节点为根的子树内 的 点对 路径距离(经过重心)小于等于K的个数,就行了。
-
(可能这句话有点绕,不会的话看看程序再理解理解,应该也是能领悟到的)
-
之后,再分治一下现在这棵子树,步骤同上。
-
一棵子树的重心其实就是你要找到一个点,使得删掉这个点后,这棵子树剩下最大(节点个数最多)联通块最小。
-
为什么每次都要算一个重心呢?你想想,要是那一棵子树刚好是一条链(就是一整条下来),而默认的根节点又在链的端点上,那么时间不就退化到了还不如暴力的程度了?于是我们求个重心,把时间复杂度保证在了
log
级别里面,就稳很多了。
程序
#include <cstdio>
#include <algorithm>
#define Max 20010
#define add(u,v,w) (To[++num]=Head[u],Head[u]=num,V[num]=v,W[num]=w)
#define For(x) for (int h=Head[x],o=V[h]; h; h=To[h],o=V[h])
#define Input for (int i=1,u,v,w; i<N; i++) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w)
using namespace std;
int N,K,root,num,ans,cnt,mins,Head[Max],To[Max],V[Max],W[Max],siz[Max],dis[Max],f[Max];
/*
程序中部分变量说明
dis[i] 所有路径到 重心 的长度
siz[i] 以 i 为根节点的子树的大小(当前重心下)
f[i] i 是否还存在(分治完一次后就把重心删掉了)
cnt 记录 dis 的个数(即路径个数)
root 当前子树的重心
maxs 当前讨论的点所有子树大小中最大值(并不是全局变量,是尝试每个重心时重新开的一个变量)
mins 讨论过的点的子树大小中最大的最小值(是全局变量,用来确定哪个才是重心)
*/
int get_size(int x,int fa){ //返回以 x 为根的子树大小,其中 x 父节点为 fa
siz[x]=1;
For(x) if (o!=fa && !f[o])
siz[x]+=get_size(o,x);
return siz[x];
}
void get_dis(int x,int d,int fa){ //x 到重心的长度为 d,之后继续 dfs
dis[++cnt]=d;
For(x) if (o!=fa && !f[o])
get_dis(o,d+W[h],x);
return;
}
void dfs_root(int x,int tot,int fa) {
//求目标子树的重心(要求除去 x 点时,它的 maxs 值最小,那么 x 就是这棵子树的重心了),其中 tot 是这棵子树的总大小(节点个数)
int maxs=tot-siz[x]; //这棵子树中x 父亲那一支先赋给 maxs
For(x) if (o!=fa && !f[o]){
maxs=max(maxs,siz[o]);
dfs_root(o,tot,x);
}
if (maxs<mins){
mins=maxs;
root=x;
}
return;
}
int work(int x,int d) {
//返回以 x 为根的子树内长度小于等于 K 的路径数(两个端点都在子树内)
//其实 d 在这里用处只有一个,是在做减法时方便把重心的儿子节点的 dis 先弄好,你也可以在分治的时候弄,不过就稍微有点麻烦了
cnt=0;
get_dis(x,d,0);
sort(dis+1,dis+cnt+1);
int daan=0,i=1,j=cnt;
while (i<j){
while (i<j && dis[i]+dis[j]>K) j--;
daan+=j-i; //相当于选一条路径 i,另一条可以为 [i+1,j] 里任意一条路径,这样得到的两个点之间长度(经过重心的那条路径)肯定是小于等于 K 的
i++;
}
return daan;
}
void dfs(int x){ //以 x 为重心分治一下
cnt=0;
mins=Max;
get_size(x,0);
dfs_root(x,siz[x],0);
ans+=work(root,0);
f[root]=1;
For(root) if (!f[o]){ //注意这里是以重心开始
ans-=work(o,W[h]); //注意,这里 dis[o] 要先赋成 W[h](即它到重心的距离)
dfs(o);
}
return;
}
int main(){
while(scanf("%d%d",&N,&K)!=EOF && N && K){
Input;
dfs(1);
printf("%d\n",ans);
num=ans=0;
for (int i=1; i<=N; i++) Head[i]=f[i]=dis[i]=0;
}
return 0;
}
时间复杂度
- 我们注意到,对于一棵大小为 n 的树,重心选好后,它的分支大小都是不大于 $\frac{n}{2} \ $ 的,那么每次分治后,联通块的大小会至少减少一半,因此最多递归分治 $\log_2 n $ 层,每次分治都会把整棵树的点都讨论一次,于是总的时间复杂度就是:
提示
- 开始我写的时候sort的边界弄错了,结果一直WA,要注意一下这些细节问题。