lintcode178. graph valid tree 圖是否是樹
【題目】
給出 n 個節點,標號分別從 0 到 n - 1 並且給出一個 無向 邊的列表 (給出每條邊的兩個頂點), 寫一個函式去判斷這張`無向`圖是否是一棵樹
假設我們不會給出重複的邊在邊的列表當中. 無向邊 [0, 1] 和 [1, 0] 是同一條邊, 因此他們不會同時出現在我們給你的邊的列表當中。
Given n
nodes
labeled from 0
to n
- 1
and a list of undirected
edges
(each edge is a pair of nodes), write a function to check whether these edges make up a valid tree.
【樣例】
給出n = 5
並且 edges
= [[0, 1], [0, 2], [0, 3], [1, 4]]
, 返回 true.
給出n = 5
並且 edges
= [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]]
, 返回 false.
1. 如果圖中邊的個數不等於點的個數n-1,必然不為樹
2. 如果有邊的兩個點屬於同一個子樹,則這個圖不為樹。
如何判斷兩個點是否屬於同一個子樹?
將有連邊的兩個點放在同一集合中(高大上的並查集)。可以使用長度為n的一維陣列existed表示點之間的關係,existed初始化為-1。對於每條邊連線的兩個點,通過判斷兩個點在existed陣列中的值判斷點是否在圖中出現過,點所屬的集合,並進行處理。
1. 如果兩個點處的值均為-1,表示兩個點都沒出現過,將兩個點在existed中的值置為較小的點,表示兩個點所在的集合;
2. 如果一個點在圖中出現過,一個點未出現過,將未出現過的點在existed中對應的值置為已出現過的點對應的值,相當於更新了點的集合;
3. 如果兩個點都出現過,合併兩個點所在集合,將兩個集合中點在existed中對應的值都換為值較小的集合。
4. 如果兩個點屬於同一個集合,圖不為樹。
AC程式碼如下(64ms)
對於一組資料,以上程式碼的執行過程如下表所示:class Solution { public: /** * @param n an integer * @param edges a list of undirected edges * @return true if it's a valid tree, or false */ bool validTree(int n, vector<vector<int>>& edges) { // Write your code here //使用n維向量儲存某個點是否出現在圖中,遍歷edges,將有連邊的點放入一個集合,如果兩個點都已經在一個集合中,返回false,existed記錄點是否已經存在在圖中 //如果邊的個數大於點的個數,返回false if (n - edges.size() != 1) return false; //屬於同一個集合的點都用數值小的點標識集合 vector<int> existed(n, -1); for (int i = 0; i < edges.size(); ++i) { int node1 = edges[i][0]; int node2 = edges[i][1]; //兩個點屬於同一集合 if (existed[node1] == existed[node2]) { if (existed[node1] == -1) //都未出現過,更新existed existed[node1] = existed[node2] = min(node1, node2); else return false; } else { //兩個點都出現過,合併點所在集合 if (existed[node1] != -1 && existed[node2] != -1) { int min = existed[node1]; int max = existed[node2]; if (existed[node1] > existed[node2]) swap(min, max); //將兩個集合合併 for (int j = 0; j < n; ++j) { if (existed[j] == max) existed[j] = min; } } else { //只有一個點出現過 if (existed[node2] == -1) existed[node2] = existed[node1]; else existed[node1] = existed[node2]; } } } return true; } };
existed
0
1
2
3
4
edges
更新existed
-1
-1
-1
-1
-1
[0, 1]
existed[0] = 0
existed[1] = 0
0
0
-1
-1
-1
[1, 2]
existed[2]= 0
0
0
0
-1
-1
[3, 4]
existed[3]= 3
existed[4]= 3
0
0
0
3
3
[1, 3]
existed[3]= 0
existed[4]= 0
-1
0
0
0
0
[2, 4]
existed[2]==existed[4]
-1
0
0
0
0
存在問題:
合併兩個集合時,需要多次遍歷existed陣列,將點所對應位置的值替換,產生了冗餘。如[3, 4]加入時,3,4對應位置都變為3,再加入[1,3]邊時,需要遍歷陣列,找到對應值為3的位置,將3換成0。
改進辦法:
通過遞迴實現深度遍歷,每次都在existed中尋找點的父節點,如果兩個點具有相同的父節點,則圖不為樹。
AC程式碼(56ms):
class Solution {
public:
/**
* @param n an integer
* @param edges a list of undirected edges
* @return true if it's a valid tree, or false
*/
int find(vector<int> &existed, int e){
//點未在圖中出現過,返回該點;否則,找到該點的父節點
if (existed[e] == -1)
return e;
else
return find(existed, existed[e]);
}
bool validTree(int n, vector<vector<int>>& edges) {
// Write your code here
//如果邊的個數不等於點的個數減一,返回false
if (n - edges.size() != 1)
return false;
vector<int> existed(n, -1);
for (int i = 0; i < edges.size(); ++i) {
int root1 = find(existed, edges[i][0]);
int root2 = find(existed, edges[i][1]);
if (root1 == root2)
return false;
//將兩個點關聯
existed[root2] = root1;
}
return true;
}
};
對於相同一組資料,執行過程如下表所示:
existed
0
1
2
3
4
edges
呼叫find
更新existed
-1
-1
-1
-1
-1
[0, 1]
find(0) = 0
existed[1] = 0
-1
0
-1
-1
-1
find(1) = 1
[1, 2]
find(1) = find(0) = 0
existed[2] = 0
-1
0
0
-1
-1
find(2) = 2
[3, 4]
find(3) = 3
existed[4] = 3
-1
0
0
-1
3
find(4) = 4
[1, 3]
find(1) = find(0) = 0
existed[3] = 0
-1
0
0
0
3
find(3) = 3
[2, 4]
find(2) = find(0) = 0
find(2) == find(4)
false
-1
0
0
0
3
find(4) = find(3) = find(0) = 0
【小結】
1. 使用一維陣列表示圖的有向邊的指向,簡單直觀,節省空間,只需要開闢大小為n的陣列,並且規定每個點對應位置的值為父節點即可。方便通過深度遍歷的方法迅速找到當前點的父節點;
2. 第一種方法在使用陣列時,將屬於同一根節點的點表示為一個集合,具體表現為這些點對應位置的值都替換為集合中當前最小的結點,每次更新陣列時需要先比較兩個節點對應位置的大小,並且合併兩個集合時都需要遍歷陣列;
3. 改進後的方法,避免了比較大小的操作和對陣列的反覆遍歷,保證每次更新操作只改變一個結點對應位置的值,將該點對應位置處的值改為當前能找到的最高父節點,但find函式遞迴實現深度遍歷,需要不斷壓棧,彈棧,產生額外地時間,空間消耗,可以將遞迴改為迴圈,避免棧的開銷。改進後的程式碼也更簡潔,易讀。
int find(vector<int> &existed, int e){
//點未在圖中出現過,返回該點;否則,找到該點的父節點
while (existed[e] != -1) {
e = existed[e];
}
return e;
}