这一篇我们看看经典又神奇的并查集,顾名思义就是并起来查,可用于处理一些不相交集合的秒杀。

一:场景 

   有时候我们会遇到这样的场景,比如: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时会存在这样的一个链表的场景。

经典算法15--并查集

从图中我们可以看到,Union时出现了最坏的情况,而且这种情况还是比较容易出现的,最终导致在Find的时候就相当寒酸苦逼了,为O(N)。

 

<2> 按秩合并

    我们发现出现这种情况的原因在于我们Union时都是将合并后的大树作为小树的孩子节点存在,那么我们在Union时能不能判断一下,

将小树作为大树的孩子节点存在,最终也就降低了新树的深度,比如图中的Union(D,{E,F})的时候可以做出如下修改。

经典算法15--并查集

可以看出,我们有效的降低了树的深度,在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上运用

伸展树的思想,这种伸展思想就是压缩路径。

经典算法15--并查集

从图中我们可以看出,当我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函数还有小的常量,嘿嘿,这

就是算法的魅力。

最后上一下总的运行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespaceConsoleApplication1
{
    classProgram
    {
        staticvoid Main(string[] args)
        {
            //定义 6 个节点
            char[] c =new char[] {'A', 'B', 'C','D', 'E', 'F'};
 
            DisjointSetset = new DisjointSet();
 
            set.Init(c);
 
            set.Union('E','F');
 
            set.Union('C','D');
 
            set.Union('C','E');
 
            varb = 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>
    publicclass DisjointSet
    {
        #region 树节点
        /// <summary>
        /// 树节点
        /// </summary>
        publicclass Node
        {
            /// <summary>
            /// 父节点
            /// </summary>
            publicchar parent;
 
            /// <summary>
            /// 节点的秩
            /// </summary>
            publicint rank;
        }
        #endregion
 
        Dictionary<char, Node> dic =new Dictionary<char, Node>();
 
        #region 做单一集合的初始化操作
        /// <summary>
        /// 做单一集合的初始化操作
        /// </summary>
        publicvoid 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>
        publicbool IsSameSet(charroot1, charroot2)
        {
            returnFind(root1) == Find(root2);
        }
        #endregion
 
        #region  查找x所属的集合
        /// <summary>
        /// 查找x所属的集合
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        publicchar Find(charx)
        {
            //如果相等,则说明已经到根节点了,返回根节点元素
            if(dic[x].parent == x)
                returnx;
 
            //路径压缩(回溯的时候赋值,最终的值就是上面返回的"x",也就是一条路径上全部被修改了)
            returndic[x].parent = Find(dic[x].parent);
        }
        #endregion
 
        #region 合并两个不相交集合
        /// <summary>
        /// 合并两个不相交集合
        /// </summary>
        /// <param name="root1"></param>
        /// <param name="root2"></param>
        /// <returns></returns>
        publicvoid Union(charroot1, charroot2)
        {
            charx1 = Find(root1);
            chary1 = 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
    }
}

经典算法15--并查集

 

相关文章: