最近被迫学习了点分治。
点分治一般用来解决这样一类问题:给出一棵树,求出这棵树上任意两个点之间距离小于等于k的点对个数。
难点在于,两个顶点之间可能会跨过根。普通的算法可能时间复杂度会很高,所以这里介绍点分治算法。
- 首先,我们有一棵树(需要根据题目情景建立,多用邻接表)↓
由于在树上求解是通过递归实现的,因此为了最大限度的简化时间复杂度,我们应该找到一种树的排列方式,使递归层数最少(也就是树的深度最小)。
这里介绍一个新的概念:树的重心。找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心。删去重心后,生成的多棵树尽可能平衡。
上图的重心就是点2,当把点2作为根节点后,整棵树变形如下:
可以发现,树的深度由4变为2。
这部分的代码实现如下:
1 void get_root(int x,int fa) 2 { 3 f[x]=0,son[x]=1; //f数组记录以x为根最大子树的大小 4 for(int i=first[x];i;i=next[i]) 5 if(to[i]!=fa && !vis[to[i]]) //不用考虑根节点以及没有访问过 6 { 7 get_root(to[i],x); 8 son[x]+=son[to[i]]; //计算x结点大小 9 f[x]=max(f[x],son[to[i]]); //找到最大子树 10 } 11 f[x]=max(f[x],sn-son[x]); 12 if(f[root]>f[x]) root=x; //更新当前根 13 }
接下来开始求解。对于一棵连通树,两个顶点之间的路径只有两种情况:经过根节点和不经过根节点
不经过根节点的情况即两个点在同一棵子树上,可以通过递归求得。所以我们只需要考虑第一种情况。
这就用简单dfs就可以了,只不过要把过程中所有经过的节点距离都记录下来(顺便说一句,这里就体现出前面费点功夫找重心的好处了,递归层数少)。
下面是一道裸题:poj1741 tree
Tree
Time Limit: 1000MS | Memory Limit: 30000K | |
Total Submissions: 25457 | Accepted: 8469 |
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.
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.
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
Source
特别注意:
如果deep[i]+deep[j]≤m,则点对(i,t)(i<t≤j)都符合题意,将j-i加入答案中,并且i++;否则j--。
然而这样还会重复计算在同一棵子树中的点对,所以在进行下一步dfs之前需要减去重复部分。(也就是无向图中会出现这种情况)
代码如下(有注释):
1 #include <iostream> 2 #include <cmath> 3 #include <cstring> 4 #include <cstdio> 5 #include <cstdlib> 6 #include <algorithm> 7 #define MAXN 10010 8 using namespace std; 9 long long m,ans; 10 int cnt,root,sn,tot; 11 int first[MAXN],to[20010],len[20010],next[20010],son[MAXN],deep[MAXN],vis[MAXN],f[MAXN],d[MAXN]; 12 void add(int x,int y,int z) //建立邻接表 13 { 14 to[++cnt]=y;len[cnt]=z; 15 next[cnt]=first[x]; //next数组记录当前起始点的前一条边的编号 16 first[x]=cnt; //first最终记录的是当前点作为起始点的最后一条边,之后从后往前遍历 17 } 18 void get_root(int x,int fa) //找重心 19 { 20 f[x]=0,son[x]=1; //f数组记录以x为根最大子树的大小 21 for(int i=first[x];i;i=next[i]) 22 if(to[i]!=fa && !vis[to[i]]) //不用考虑根节点以及没有访问过 23 { 24 get_root(to[i],x); 25 son[x]+=son[to[i]]; //计算x结点大小 26 f[x]=max(f[x],son[to[i]]); //找到最大子树 27 } 28 f[x]=max(f[x],sn-son[x]); 29 if(f[root]>f[x]) root=x; //更新当前根 30 } 31 void get_deep(int x,int fa) //求当前x为根节点的树的深度 32 { 33 d[++tot]=deep[x]; //每两个点之间的距离值都要记录 34 for(int i=first[x];i;i=next[i]) 35 if(to[i]!=fa && !vis[to[i]]) 36 { 37 deep[to[i]]=deep[x]+len[i]; 38 get_deep(to[i],x); 39 } 40 } 41 int calc(int x) 42 { 43 tot=0; 44 get_deep(x,0); 45 sort(d+1,d+tot+1); 46 int i=1,j=tot,sum=0; 47 while(i<j) //计算个数 48 { 49 if(d[i]+d[j]<=m){sum+=j-i,i++;} 50 else j--; 51 } 52 return sum; 53 } 54 void dfs(int x) 55 { 56 deep[x]=0; 57 vis[x]=1; 58 ans+=calc(x); 59 for(int i=first[x];i;i=next[i]) 60 if(!vis[to[i]]) 61 { 62 deep[to[i]]=len[i]; 63 ans-=calc(to[i]); //计算不符合题意的答案 64 sn=son[to[i]]; 65 root=0; 66 get_root(to[i],0); 67 dfs(root); 68 } 69 } 70 int main() 71 { 72 int n,x,y,z; 73 while(scanf("%d%lld",&n,&m)) 74 { 75 if(n==0 && m==0) break; 76 memset(first,0,sizeof(first)); 77 memset(vis,0,sizeof(vis)); 78 cnt=0,ans=0; 79 for(int i=1;i<n;i++) 80 { 81 scanf("%d%d%d",&x,&y,&z); 82 add(x,y,z); //无向图,建两次 83 add(y,x,z); 84 } 85 f[0]=0x7fffffff;sn=n; 86 root=0;get_root(1,0);dfs(root); 87 printf("%lld ",ans); 88 } 89 return 0; 90 }