經典算法題每日演練——第十五題 並查集


    這一篇我們看看經典又神奇的並查集,顧名思義就是並起來查,可用於處理一些不相交集合的秒殺。

一:場景 

   有時候我們會遇到這樣的場景,比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判斷{1,2}是否屬於同一個集合,當然實現方法

有很多,一般情況下,普通青年會做出O(MN)的復雜度,那么有沒有更輕量級的復雜度呢?嘿嘿,並查集就是用來解決這個問題的。

 

二:操作

  從名字可以出來,並查集其實只有兩種操作,並(Union)和查(Find),並查集是一種算法,所以我們要給它選擇一個好的數據結構,

通常我們用樹來作為它的底層實現。

1.節點定義

 1         #region 樹節點
 2         /// <summary>
 3         /// 樹節點
 4         /// </summary>
 5         public class Node
 6         {
 7             /// <summary>
 8             /// 父節點
 9             /// </summary>
10             public char parent;
11 
12             /// <summary>
13             /// 節點的秩
14             /// </summary>
15             public int rank;
16         }
17         #endregion

 

2.Union操作

 <1>原始方案

      首先我們會對集合的所有元素進行打散,最后每個元素都是一個獨根的樹,然后我們Union其中某兩個元素,讓他們成為一個集合,

 最壞情況下我們進行M次的Union時會存在這樣的一個鏈表的場景。

從圖中我們可以看到,Union時出現了最壞的情況,而且這種情況還是比較容易出現的,最終導致在Find的時候就相當寒酸苦逼了,為O(N)。

 

<2> 按秩合並

    我們發現出現這種情況的原因在於我們Union時都是將合並后的大樹作為小樹的孩子節點存在,那么我們在Union時能不能判斷一下,

將小樹作為大樹的孩子節點存在,最終也就降低了新樹的深度,比如圖中的Union(D,{E,F})的時候可以做出如下修改。

可以看出,我們有效的降低了樹的深度,在N個元素的集合中,構建樹的深度不會超過LogN層。M次操作的復雜度為O(MlogN),從代

碼上來說,我們用Rank來統計樹的秩,可以理解為樹的高度,獨根樹時Rank=0,當兩棵樹的Rank相同時,可以隨意挑選合並,在新

根中的Rank++就可以了。

 1 #region 合並兩個不相交集合
 2         /// <summary>
 3         /// 合並兩個不相交集合
 4         /// </summary>
 5         /// <param name="root1"></param>
 6         /// <param name="root2"></param>
 7         /// <returns></returns>
 8         public void Union(char root1, char root2)
 9         {
10             char x1 = Find(root1);
11             char y1 = Find(root2);
12 
13             //如果根節點相同則說明是同一個集合
14             if (x1 == y1)
15                 return;
16 
17             //說明左集合的深度 < 右集合
18             if (dic[x1].rank < dic[y1].rank)
19             {
20                 //將左集合指向右集合
21                 dic[x1].parent = y1;
22             }
23             else
24             {
25                 //如果 秩 相等,則將 y1 並入到 x1 中,並將x1++
26                 if (dic[x1].rank == dic[y1].rank)
27                     dic[x1].rank++;
28 
29                 dic[y1].parent = x1;
30             }
31         }
32         #endregion

 

 3.Find操作

   我們學算法,都希望能把一個問題優化到地球人都不能優化的地步,針對logN的級別,我們還能優化嗎?當然可以。

 <1>路徑壓縮

     在Union和Find這兩種操作中,顯然我們在Union上面已經做到了極致,下面我們在Find上面考慮一下,是不是可以在Find上運用

伸展樹的思想,這種伸展思想就是壓縮路徑。

從圖中我們可以看出,當我Find(F)的時候,找到“F”后,我們開始一直回溯,在回溯的過程中給,把該節點的父親指向根節點。最終

我們會形成一個壓縮后的樹,當我們再次Find(F)的時候,只要O(1)的時間就可以獲取,這里有個注意的地方就是Rank,當我們在路

徑壓縮時,最后樹的高度可能會降低,可能你會意識到原先的Rank就需要修改了,所以我要說的就是,當路徑壓縮時,Rank保存的就

是樹高度的上界,而不僅僅是明確的樹高度,可以理解成"伸縮椅"伸時候的長度。

 1 #region  查找x所屬的集合
 2         /// <summary>
 3         /// 查找x所屬的集合
 4         /// </summary>
 5         /// <param name="x"></param>
 6         /// <returns></returns>
 7         public char Find(char x)
 8         {
 9             //如果相等,則說明已經到根節點了,返回根節點元素
10             if (dic[x].parent == x)
11                 return x;
12 
13             //路徑壓縮(回溯的時候賦值,最終的值就是上面返回的"x",也就是一條路徑上全部被修改了)
14             return dic[x].parent = Find(dic[x].parent);
15         }
16         #endregion

我們注意到,在路徑壓縮后,我們將LogN的復雜度降低到Alpha(N),Alpha(N)可以理解成一個比hash函數還有小的常量,嘿嘿,這

就是算法的魅力。

最后上一下總的運行代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            //定義 6 個節點
            char[] c = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };

            DisjointSet set = new DisjointSet();

            set.Init(c);

            set.Union('E', 'F');

            set.Union('C', 'D');

            set.Union('C', 'E');

            var b = set.IsSameSet('C', 'E');

            Console.WriteLine("C,E是否在同一個集合:{0}", b);

            b = set.IsSameSet('A', 'C');

            Console.WriteLine("A,C是否在同一個集合:{0}", b);

            Console.Read();
        }
    }

    /// <summary>
    /// 並查集
    /// </summary>
    public class DisjointSet
    {
        #region 樹節點
        /// <summary>
        /// 樹節點
        /// </summary>
        public class Node
        {
            /// <summary>
            /// 父節點
            /// </summary>
            public char parent;

            /// <summary>
            /// 節點的秩
            /// </summary>
            public int rank;
        }
        #endregion

        Dictionary<char, Node> dic = new Dictionary<char, Node>();

        #region 做單一集合的初始化操作
        /// <summary>
        /// 做單一集合的初始化操作
        /// </summary>
        public void Init(char[] c)
        {
            //默認的不想交集合的父節點指向自己
            for (int i = 0; i < c.Length; i++)
            {
                dic.Add(c[i], new Node()
                {
                    parent = c[i],
                    rank = 0
                });
            }
        }
        #endregion

        #region 判斷兩元素是否屬於同一個集合
        /// <summary>
        /// 判斷兩元素是否屬於同一個集合
        /// </summary>
        /// <param name="root1"></param>
        /// <param name="root2"></param>
        /// <returns></returns>
        public bool IsSameSet(char root1, char root2)
        {
            return Find(root1) == Find(root2);
        }
        #endregion

        #region  查找x所屬的集合
        /// <summary>
        /// 查找x所屬的集合
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        public char Find(char x)
        {
            //如果相等,則說明已經到根節點了,返回根節點元素
            if (dic[x].parent == x)
                return x;

            //路徑壓縮(回溯的時候賦值,最終的值就是上面返回的"x",也就是一條路徑上全部被修改了)
            return dic[x].parent = Find(dic[x].parent);
        }
        #endregion

        #region 合並兩個不相交集合
        /// <summary>
        /// 合並兩個不相交集合
        /// </summary>
        /// <param name="root1"></param>
        /// <param name="root2"></param>
        /// <returns></returns>
        public void Union(char root1, char root2)
        {
            char x1 = Find(root1);
            char y1 = Find(root2);

            //如果根節點相同則說明是同一個集合
            if (x1 == y1)
                return;

            //說明左集合的深度 < 右集合
            if (dic[x1].rank < dic[y1].rank)
            {
                //將左集合指向右集合
                dic[x1].parent = y1;
            }
            else
            {
                //如果 秩 相等,則將 y1 並入到 x1 中,並將x1++
                if (dic[x1].rank == dic[y1].rank)
                    dic[x1].rank++;

                dic[y1].parent = x1;
            }
        }
        #endregion
    }
}

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM