[转]编程之美 2013 全国挑战赛 资格赛 题目三 树上的三角形
题目三 树上的三角形
时间限制: 2000ms 内存限制: 256MB
描述
有一棵树,树上有只毛毛虫。它在这棵树上生活了很久,对它的构造了如指掌。所以它在树上从来都是走最短路,不会绕路。它还还特别喜欢三角形,所以当它在树上爬来爬去的时候总会在想,如果把刚才爬过的那几根树枝/树干锯下来,能不能从中选三根出来拼成一个三角形呢?
输入
输入数据的第一行包含一个整数 T,表示数据组数。
接下来有 T 组数据,每组数据中:
第一行包含一个整数 N,表示树上节点的个数(从 1 到 N 标号)。
接下来的 N-1 行包含三个整数 a, b, len,表示有一根长度为 len 的树枝/树干在节点 a 和节点 b 之间。
接下来一行包含一个整数 M,表示询问数。
接下来M行每行两个整数 S, T,表示毛毛虫从 S 爬行到了 T,询问这段路程中的树枝/树干是否能拼成三角形。
输出
对于每组数据,先输出一行"Case #X:",其中X为数据组数编号,从 1 开始。
接下来对于每个询问输出一行,包含"Yes"或“No”,表示是否可以拼成三角形。
数据范围
1 ≤ T ≤ 5
小数据:1 ≤ N ≤ 100, 1 ≤ M ≤ 100, 1 ≤ len ≤ 10000
大数据:1 ≤ N ≤ 100000, 1 ≤ M ≤ 100000, 1 ≤ len ≤ 1000000000
样例输入
2
5
1 2 5
1 3 20
2 4 30
4 5 15
2
3 4
3 5
5
1 4 32
2 3 100
3 5 45
4 5 60
2
1 4
1 3
样例输出
Case #1:
No
Yes
Case #2:
No
Yes
解题思路
这道题如果直接按照题意去写,那么可以利用广度优先搜索得到最短路径(因为这是一颗树,而不是图,所以不必使用最短路算法),然后判断路径上的边是否能组成一个三角形(先对路径排序,然后用两边之和大于第三边进行判断)。不过搜索的时间复杂度是 O(N),判断三角形的时间复杂度为 O(llgl)(其中 l 是最短路径的长度),小数据没问题,但大数据肯定会挂。
判断三角形是否存在,我并没有更好的办法,那么只能在求最短路径上下手了,以下面的树作为例子(题目没说是几叉树,不过没有关系):

图 1 一颗树的示例
求一棵树上两个节点的最短路径,其实就是求两个节点的最近公共祖先(Least Common Ancestors,LCA)。最近公共祖先指的是在一颗有根树中,找到两个节点 u 和 v 最近的公共祖先。这个概念很容易理解,例如上面节点 5 和 10 的 LCA 就是 1,3 和 11 的 LCA 是 3,7 和 9 的 LCA 是 3。
显然,两个节点与它们的最近公共祖先之间的路径(可以不断向上查找父节点得到)加起来,就是两个节点间的最短路径。上面节点 5 和 10 的最短路径就为 5、2、1、3、8、10;节点 3 和 11 的最短路径就为 3、8、9、11。
求 LCA 有两种算法,一种是离线的 Tarjan 算法,计算出所有 M 个询问所需的时间复杂度是 O(N+M);另一种是基于区间最值查询(Range Minimum/Maximum Query,RMQ)的在线算法,预处理时间是 O(NlgN),每次询问的时间复杂度为 O(1),总得时间复杂度就是 O(NlgN+M)。两个算法使用那个都可以,不过感觉还是用 Tarjan 更好点,占用内存更少,速度也更快。关于这两个算法的详细解释,可以参见算法之LCA与RMQ问题,这里就不详细说明了。
在线算法的代码
View Code离线算法的代码
View Code这两个算法应该是没问题的,但大数据的时候都 TLE 了,看来 list 真不能随便用,动态开辟内存还是太慢了。离线算法的内存使用大概只有在线算法的 70%。
后来我翻代码的时候(所有人的代码都可以看到,这点挺给力),看到有人没用上面的 LCA 算法,而是在用 DFS 建好树后,使要判断的两个节点 u 和 v 分别沿着父节点链向上遍历,同时保持 u 和 v 的深度是相同的,这样同样能得到最短路径和 LCA,只不过时间复杂度要高一些。但在这道题中也没有关系,因为在找三角形时还是需要把路径遍历一编才可以,LCA 的计算反而会带来额外的复杂性,看来的确是自己想复杂了。
这段遍历算法大概类似于下面这样:
1 while (deep(u) > deep(v)){ 2 // 记录路径 u 到 parent(u) 的路径 3 u = parent(u); 4 } 5 while (deep(v) > deep(u)){ 6 // 记录路径 v 到 parent(v) 的路径 7 v = parent(v); 8 } 9 while (u != v){ 10 // 记录路径 u 到 parent(u) 的路径 11 u = parent(u); 12 // 记录路径 v 到 parent(v) 的路径 13 v = parent(v); 14 }
完整的代码见这里,ID 是 mochavic(排名第一),果然是大神。
还是把 mochavic 的代码也贴这里吧:
View Code
1 #include <stdio.h> 2 #include <vector> 3 #include <algorithm> 4 using namespace std; 5 int deep[100010], f[100010][2]; 6 vector<int> e[100010]; 7 int c[60], cn; 8 void dfs(int fa, int x, int d){ 9 int i, y; 10 deep[x] = d; 11 for (i = 0; i < (int)e[x].size(); i += 2){ 12 y = e[x][i]; 13 if (y == fa) continue; 14 f[y][0] = x; 15 f[y][1] = e[x][i + 1]; 16 dfs(x, y, d + 1); 17 } 18 } 19 void pd(int x, int y){ 20 while (deep[x] > deep[y]){ 21 c[cn++] = f[x][1]; 22 x = f[x][0]; 23 if (cn == 50) return; 24 } 25 while (deep[y] > deep[x]){ 26 c[cn++] = f[y][1]; 27 y = f[y][0]; 28 if (cn == 50) return; 29 } 30 while (x != y){ 31 c[cn++] = f[x][1]; 32 x = f[x][0]; 33 c[cn++] = f[y][1]; 34 y = f[y][0]; 35 if (cn >= 50) return; 36 } 37 } 38 int main(){ 39 int T, ri = 1, n, m, x, y, z, i; 40 scanf("%d", &T); 41 while (T--){ 42 scanf("%d", &n); 43 for (i = 1; i <= n; i++) e[i].clear(); 44 for (i = 1; i < n; i++){ 45 scanf("%d%d%d", &x, &y, &z); 46 e[x].push_back(y); 47 e[x].push_back(z); 48 e[y].push_back(x); 49 e[y].push_back(z); 50 } 51 dfs(0, 1, 0); 52 printf("Case #%d:\n", ri++); 53 scanf("%d", &m); 54 while (m--){ 55 scanf("%d%d", &x, &y); 56 cn = 0; 57 pd(x, y); 58 sort(c, c + cn); 59 for (i = 0; i + 2 < cn; i++){ 60 if (c[i] + c[i + 1] > c[i + 2]) break; 61 } 62 if (i + 2 < cn) printf("Yes\n"); 63 else printf("No\n"); 64 } 65 } 66 return 0; 67 }
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。


浙公网安备 33010602011771号