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;
}



發佈了41 篇原創文章 · 獲贊 41 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章