程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 題目三 樹上的三角形

題目三 樹上的三角形

編輯:C++入門知識

描述
有一棵樹,樹上有只毛毛蟲。它在這棵樹上生活了很久,對它的構造了如指掌。所以它在樹上從來都是走最短路,不會繞路。它還還特別喜歡三角形,所以當它在樹上爬來爬去的時候總會在想,如果把剛才爬過的那幾根樹枝/樹干鋸下來,能不能從中選三根出來拼成一個三角形呢?

輸入
輸入數據的第一行包含一個整數 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

樣例輸入
251 2 51 3 202 4 304 5 1523 43 551 4 322 3 1003 5 454 5 6021 41 3樣例輸出
Case #1:NoYesCase #2:NoYes解題思路
這道題如果直接按照題意去寫,那麼可以利用廣度優先搜索得到最短路徑(因為這是一顆樹,而不是圖,所以不必使用最短路算法),然後判斷路徑上的邊是否能組成一個三角形(先對路徑排序,然後用兩邊之和大於第三邊進行判斷)。不過搜索的時間復雜度是 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
  1 #include <stdio.h>
  2 #include <cmath>
  3 #include <algorithm>
  4 #include <list>
  5 #include <string.h>
  6 using namespace std;
  7 // 樹的節點
  8 struct Node {
  9     int next, len;
 10     Node (int n, int l):next(n), len(l) {}
 11 };
 12 int pow2[20];
 13 list<Node> nodes[100010];
 14 bool visit[100010];
 15 int ns[200010];
 16 int nIdx;
 17 int length[100010];
 18 int parent[100010];
 19 int depth[200010];
 20 int first[100010];
 21 int mmin[20][200010];
 22 int edges[100010];
 23 // DFS 對樹進行預處理
 24 void dfs(int u, int dep)
 25 {
 26     ns[++nIdx] = u; depth[nIdx] = dep;
 27     visit[u] = true;
 28     if (first[u] == -1) first[u] = nIdx;
 29     list<Node>::iterator it = nodes[u].begin(), end = nodes[u].end();
 30     for (;it != end; it++)
 31     {
 32         int v = it->next;
 33         if(!visit[v])
 34         {
 35             length[v] = it->len;
 36             parent[v] = u;
 37             dfs(v, dep + 1);
 38             ns[++nIdx] = u;
 39             depth[nIdx] = dep;
 40         }
 41     }
 42 }
 43 // 初始化 RMQ
 44 void init_rmq()
 45 {
 46     nIdx = 0;
 47     memset(visit, 0, sizeof(visit));
 48     memset(first, -1, sizeof(first));
 49     depth[0] = 0;
 50     length[1] = parent[1] = 0;
 51     dfs(1, 1);
 52     memset(mmin, 0, sizeof(mmin));
 53     for(int i = 1; i <= nIdx; i++) {
 54         mmin[0][i] = i;
 55     }
 56     int t1 = (int)(log((double)nIdx) / log(2.0));
 57     for(int i = 1; i <= t1; i++) {
 58         for(int j = 1; j + pow2[i] - 1 <= nIdx; j++) {
 59             int a = mmin[i-1][j], b = mmin[i-1][j+pow2[i-1]];
 60             if(depth[a] <= depth[b]) {
 61                 mmin[i][j] = a;
 62             } else {
 63                 mmin[i][j] = b;
 64             }
 65         }
 66     }
 67 }
 68 // RMQ 詢問
 69 int rmq(int u, int v)
 70 {
 71     int i = first[u], j = first[v];
 72     if(i > j) swap(i, j);
 73     int t1 = (int)(log((double)j - i + 1) / log(2.0));
 74     int a = mmin[t1][i], b = mmin[t1][j - pow2[t1] + 1];
 75     if(depth[a] <= depth[b]) {
 76         return ns[a];
 77     } else {
 78         return ns[b];
 79     }
 80 }
 81
 82 int main() {
 83     for(int i = 0; i < 20; i++) {
 84          pow2[i] = 1 << i;
 85     }
 86     int T, n, m, a, b, len;
 87     scanf("%d ", &T);
 88     for (int caseIdx = 1;caseIdx <= T;caseIdx++) {
 89         scanf("%d", &n);
 90         for (int i = 0;i <= n;i++) {
 91             nodes[i].clear();
 92         }
 93         for (int i = 1;i < n;i++) {
 94             scanf("%d%d%d", &a, &b, &len);
 95             nodes[a].push_back(Node(b, len));
 96             nodes[b].push_back(Node(a, len));
 97         }
 98         init_rmq();
 99         scanf("%d", &m);
100         printf("Case #%d:\n", caseIdx);
101         for (int i = 0;i < m;i++) {
102             scanf("%d%d", &a, &b);
103             // 利用 RMQ 得到 LCA
104             int root = rmq(a, b);
105             bool success = false;
106             int l = 0;
107             while (a != root) {
108                 edges[l++] = length[a];
109                 a = parent[a];
110             }
111             while (b != root) {
112                 edges[l++] = length[b];
113                 b = parent[b];
114             }
115             if (l >= 3) {
116                 sort(edges, edges + l);
117                 for (int j = 2;j < l;j++) {
118                     if (edges[j - 2] + edges[j - 1] > edges[j]) {
119                         success = true;
120                         break;
121                     }
122                 }
123             }
124             if (success) {
125                 puts("Yes");
126             } else {
127                 puts("No");
128             }
129         }
130     }
131     return 0;
132 }離線算法的代碼

View Code
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <list>
  4 #include <algorithm>
  5 using namespace std;
  6 // 樹和查詢的節點
  7 struct Node {
  8     int next, len;
  9     Node (int n, int l):next(n), len(l) {}
 10 };
 11 list<Node> nodes[100010];
 12 list<Node> querys[100010];
 13 bool visit[100010];
 14 int ancestor[100010];
 15 int parent[100010];
 16 int length[100010];
 17 int edges[100010];
 18 // 查詢的結果
 19 bool result[100010];
 20 // 並查集
 21 int uset[100010];
 22 int find(int x) {
 23     int p = x, t;
 24     while (uset[p] >= 0) p = uset[p];
 25     while (x != p) { t = uset[x]; uset[x] = p; x = t; }
 26     return x;
 27 }
 28 void un_ion(int a, int b) {
 29     if ((a = find(a)) == (b = find(b))) return;
 30     if (uset[a] < uset[b]) { uset[a] += uset[b]; uset[b] = a; }
 31     else { uset[b] += uset[a]; uset[a] = b; }
 32 }
 33 void init_uset() {
 34     memset(uset, -1, sizeof(uset));
 35 }
 36
 37 void tarjan(int u) {
 38     visit[u] = true;
 39     ancestor[find(u)] = u;
 40     list<Node>::iterator it = nodes[u].begin(), end = nodes[u].end();
 41     for (;it != end; it++)
 42     {
 43         int v = it->next;
 44         if(!visit[v])
 45         {
 46             length[v] = it->len;
 47             parent[v] = u;
 48             tarjan(v);
 49             un_ion(u, v);
 50             ancestor[find(u)] = u;
 51         }
 52     }
 53     it = querys[u].begin(); end = querys[u].end();
 54     for (;it != end; it++)
 55     {
 56         int v = it->next;
 57         if(visit[v])
 58         {
 59             // 處理從 u 起始的查詢
 60             int root = ancestor[find(v)];
 61             int l = 0;
 62             int a = u;
 63             while (a != root) {
 64                 edges[l++] = length[a];
 65                 a = parent[a];
 66             }
 67             while (v != root) {
 68                 edges[l++] = length[v];
 69                 v = parent[v];
 70             }
 71             sort(edges, edges + l);
 72             for (int j = 2;j < l;j++) {
 73                 if (edges[j - 2] + edges[j - 1] > edges[j]) {
 74                     result[it->len] = true;
 75                     break;
 76                 }
 77             }
 78         }
 79     }
 80 }
 81
 82 int main() {
 83     int T, n, m, a, b, len;
 84     scanf("%d ", &T);
 85     for (int caseIdx = 1;caseIdx <= T;caseIdx++) {
 86         scanf("%d", &n);
 87         for (int i = 0;i <= n;i++) {
 88             nodes[i].clear();
 89             querys[i].clear();
 90         }
 91         for (int i = 1;i < n;i++) {
 92             scanf("%d%d%d", &a, &b, &len);
 93             nodes[a].push_back(Node(b, len));
 94             nodes[b].push_back(Node(a, len));
 95         }
 96         scanf("%d", &m);
 97         for (int i = 0;i < m;i++) {
 98             scanf("%d%d", &a, &b);
 99             // 查詢要添加兩遍,以防止出現遺漏
100             querys[a].push_back(Node(b, i));
101             querys[b].push_back(Node(a, i));
102         }
103         printf("Case #%d:\n", caseIdx);
104         init_uset();
105         memset(visit, 0, sizeof(visit));
106         memset(result, 0, sizeof(result));
107         length[1] = parent[1] = 0;
108         tarjan(1);
109         for (int i = 0;i < m;i++) {
110             if (result[i]) {
111                 puts("Yes");
112             } else {
113                 puts("No");
114             }
115         }
116     }
117     return 0;
118 }這兩個算法應該是沒問題的,但大數據的時候都 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(排名第一),果然是大神。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved