最小生成樹與並查集(leetcode684,685, 721)

最小生成樹

說道並查集,不得不提的是最小生成樹,因爲並查集的最經典的應用就是解決最小生成樹的Kruskal算法。

有兩個經典的算法可以用來解決最小生成樹問題:Kruskal算法和Prim算法。其中Kruskal算法中便應用了並查集這種數據結構。

Kruskal算法

  • 新建圖G,G中擁有原圖中相同的結點,但是沒有邊
  • 將原圖中所有邊按照權值從小到大排序
  • 從權值最小的邊開始,如果這條邊連接的兩個節點不在一個連通分量中(也就是不形成環),則添加這條邊到圖G中
  • 重複3,直至圖G中所有的節點都在同一個連通分量中
  • 在這裏插入圖片描述
    Kruskal算法是一種貪心算法,並且已經唄證明最終能夠收斂到最好的結果。在實現Krusal算法時,則需要用到並查集這種數據結構來減小算法的時間複雜度,下面將詳細介紹這種數據結構。

Prime算法

實現最小生成樹還有一種算法叫做Prime算法,Prime算法維護的是頂點的集合,而Kruskal維護的是邊的集合。
Prime算法過程:

  • 輸入:頂點集合爲VV,邊集合爲EE
  • 初始化:Vnew=nullEnew=nullV_{new}=null, E_{new}=null
  • 隨機選擇一個結點v加入到VnewV_{new}
  • 重複下列操作,直到Vnew=VV_{new}=V
    1. 在集合EE中選取權值最小的邊(u,v)(u, v),其中uu爲集合VnewV_{new}中的元素,而vvVVnewV-V_{new}中的頂點;
    2. 將vv加入到集合VnewV_{new}中,將(u,v)(u,v)加入到邊集合EnewE_{new}中;
  • 輸出:使用集合VnewV_{new}EnewE_{new}來描述所得到的最小生成樹
    merge在這裏插入圖片描述

並查集

Kruskal算法中需要判斷兩個節點是否在同一個連通分量中,如何判斷是否是一個連通分量呢??也就判斷是否加入新邊之後,是否會和原來已經添加的邊形成環路,並查集正是高效的實現了這個功能。

三個操作

  • MakeSet是初始化操作,即爲每一個node創建一個連通分量,且這個node爲這個連通分量的代表,這裏的連通分量的代表指的是當連通分量中有多個點中,需要從這些點中選出一個點來代表這個連通分量,而這個點也被稱爲這個連通分量的parent;
  • Find是指找到這個點所屬的連通分量的parent;
  • Union是指將兩個連通分量合併成一個連通分量,並選出代表這個連通分量的新的parent;

如何通過以上操作判斷某條邊是否會與原來的邊集形成環路呢?

  1. 給定一條邊,爲這兩條邊的兩個頂點執行find操作,如果兩個頂點的parent一樣,那麼就說明這兩個點已經在同一個連通分量中,再添加就會導致閉環,不添加該邊;
  2. 當兩個點的parent不同時,即兩個點在不同的連通分量中,需要通過union操作將這兩個連通分量連起來;
  3. 重複1,2步操作直到所有的邊遍歷完;

具體題目

leetcode 684

684.Redundant Connection這道題目實際上就是要找到一個無向圖中形成環路的最後那條邊(輸入保證了所有邊會形成迴路)

vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        vector<int> root(2000, 0);
        for(int i=0; i<2000; i++)
            root[i]=i;
        vector<int> res;
        for(auto edge : edges)
        {
            int a = edge[0];
            int b = edge[1];
            #find
            while(root[a]!=a)
                a = root[a];
            while(root[b]!=b)
                b = root[b];
            #union
            if(a==b)
                res = edge;
            else
                root[a]=b;
        }
        return res;
        
    }

首先初始化,將每個頂點設爲單獨的連通分量,每個頂點的根節點爲自己。
每次find操作的時間複雜度爲O(n)O(n)
每次union的時間複雜度爲O(1)O(1)
將b結點所在連通子圖的根節點當做a結點所在聯通子圖的根節點的根節點,也就是將a結點所在的聯通子圖當做b的根節點的子樹。這樣就將兩個連通子圖連通爲一個連通子圖。
所以總的時間複雜度爲O(mn)O(mn),那麼有沒有一種改進總體時間複雜度的方法呢?

path compression和union by rank

Path compression:
將連通分量看作爲一棵樹,在循環的find操作中,將樹中的每個結點都連接到parent結點,從而降低樹的高度。

降低樹的高度就是能夠降低查找的時間複雜度,從O(n)O(n)降爲了O(logn)O(logn),因爲原來的遞歸搜索實際上是在每個結點只有一個子節點的樹上進行搜索,樹的高度即爲結點的個數,而通過path compression則能夠有效的降低樹的高度。

Union by rank:
另外一個問題就是進行Union操作時,需要將高度低的樹連接到高度教高的樹上,目的是減少union後的整棵樹的高度。rank代表的就是樹的高度。

採用了path compression和union by rank之後,find的時間複雜度變爲了O(logn)O(logn),union的時間複雜度爲O(1)O(1),因此總的時間複雜度爲O(mlogn)O(mlogn)mm爲邊的數目,而nn爲點的數目。改進後的代碼如下:

int root[1001];
    int rank[1001];
    int find(int node)
    {
	    int ans = node;
	    while(root[node]!=node)
		    node = root[node];
	    int r = node;
	    while(root[ans]!=r)
	    {
		    int tmp = ans;
		    ans = root[ans];
		    root[tmp] = r;
	    }
	    return r;
    }
    vector<int> findRedundantConnection(vector<vector<int>>& edges)
    {
        
        for(int i=0; i<1000; i++)
        {
            root[i]=i;
            rank[i]=0;
        }
        vector<int> res;
        for(auto edge : edges)
        {
            int a = edge[0];
            int b = edge[1];
            #find
            int p1 = find(a);
            int p2 = find(b);
            #union
            if(p1==p2)
                res = edge;
            else if(rank[p1]>rank[p2])
            {
                root[p2] = p1;
            }
            else if(rank[p1]<rank[p2])
            {
                root[p1] = p2;
            }
            else
            {
                rank[p2]+=1;
                root[p1]=p2;
            }
        }
        return res;
    }

leecode 685

685. Redundant Connection ||從前面的無向圖升級到了有向圖,對應的要求從原來的僅要求不形成環路升級到在不形成環路的基礎上,拓撲必須要是一棵合法樹,也就是每個點只能有一個父節點,例如 [[2,1],[3,1]] 這兩條邊雖然沒有形成環路,但是 1 有兩個父親節點(2和3),因此不是一棵合法的樹。

由於題目說明了輸入只有一條不合法的邊,因此首先可以統計一下這些邊中是否存在某個點有兩個父親節點,假如有,則需要移除的邊必定爲連着這個點的兩條邊中的一條,通過上面 Union-find 的方法,可以判斷出假如移除掉連着這個點的第一條邊時,是否會形成迴路。如果會,則說明需要移除第二條邊,否則直接移除第一條邊。

leetcode 721

721. Accounts-merge該任務的任務是連接同一個account的email,這非非常適用於並查集來實現。爲了將這些emails進行group,每個group需要有一個代表(父節點)。在最初, 每個email是其自己的代表。每個accont中的emails很自然的屬於同一個group,應該被分配到相同的parent。選擇每個account中的第一個email作爲父節點。在其後進行find和union操作進行查找和更新父節點。

class Solution {
public:
    vector<vector<string>> accountsMerge(vector<vector<string>>& acts) {
        map<string, string> owner;
        map<string, string> parents;
        map<string, set<string>> unions;
        for (int i = 0; i < acts.size(); i++) {
            for (int j = 1; j < acts[i].size(); j++) {
                parents[acts[i][j]] = acts[i][j];
                owner[acts[i][j]] = acts[i][0];
            }
        }
        for (int i = 0; i < acts.size(); i++) {
            string p = find(acts[i][1], parents);
            for (int j = 2; j < acts[i].size(); j++)
                parents[find(acts[i][j], parents)] = p;
        }
        for (int i = 0; i < acts.size(); i++)
            for (int j = 1; j < acts[i].size(); j++)
                unions[find(acts[i][j], parents)].insert(acts[i][j]);

        vector<vector<string>> res;
        for (pair<string, set<string>> p : unions) {
            vector<string> emails(p.second.begin(), p.second.end());
            emails.insert(emails.begin(), owner[p.first]);
            res.push_back(emails);
        }
        return res;
    }
private:
    string find(string s, map<string, string>& p) {
        return p[s] == s ? s : find(p[s], p);
    }
};

參考資料

吳良超的leetcode解題報告

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章