经典算法题每日演练——第十五题 并查集

简介: 原文:经典算法题每日演练——第十五题 并查集    这一篇我们看看经典又神奇的并查集,顾名思义就是并起来查,可用于处理一些不相交集合的秒杀。 一:场景     有时候我们会遇到这样的场景,比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判断{1,2}是否属于同一个集合,当然实现方法 有很多,一般情况下,普通青年会做出O(MN)的复杂度,那么有没有更轻量级的复杂度呢?嘿嘿,并查集就是用来解决这个问题的。
原文: 经典算法题每日演练——第十五题 并查集

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

一:场景 

   有时候我们会遇到这样的场景,比如: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
    }
}

  

 

目录
相关文章
|
机器学习/深度学习 存储 算法
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
【算法沉淀】刷题笔记:并查集 带权并查集+实战讲解
|
存储 算法 iOS开发
【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量(通俗易懂版)
【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量(通俗易懂版)
|
算法 JavaScript 前端开发
第一个算法项目 | JS实现并查集迷宫算法Demo学习
本文是关于使用JavaScript实现并查集迷宫算法的中国象棋demo的学习记录,包括项目运行方法、知识点梳理、代码赏析以及相关CSS样式表文件的介绍。
178 7
第一个算法项目 | JS实现并查集迷宫算法Demo学习
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
216 1
|
大数据 UED 开发者
实战演练:利用Python的Trie树优化搜索算法,性能飙升不是梦!
在数据密集型应用中,高效搜索算法至关重要。Trie树(前缀树/字典树)通过优化字符串处理和搜索效率成为理想选择。本文通过Python实战演示Trie树构建与应用,显著提升搜索性能。Trie树利用公共前缀减少查询时间,支持快速插入、删除和搜索。以下为简单示例代码,展示如何构建及使用Trie树进行搜索与前缀匹配,适用于自动补全、拼写检查等场景,助力提升应用性能与用户体验。
342 2
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
在编程与算法的广袤天地中,总有一些工具如同神兵利器,能够助你一臂之力,在复杂的问题前游刃有余。今天,我们就来深入探讨这样一件神器——Python并查集(Union-Find),看看它是如何让你在算法界呼风唤雨,轻松应对各种复杂场景的。
247 2
|
算法 计算机视觉 Python
Python并查集大揭秘:让你在算法界呼风唤雨,秒杀一切复杂场景!
【7月更文挑战第18天】并查集是Python中解决集合动态合并与查询的利器,常用于复杂问题。例如,在社交网络中快速判断用户是否在同一朋友圈,通过路径压缩优化的`UnionFind`类实现。另外,计算图像中岛屿数量也可借助并查集,将相邻像素合并成集合。并查集的应用显示了其在算法中的高效和灵活性,是提升编程技能的关键工具。
211 2
|
存储 算法 Python
火箭般的提升!学会Python并查集,让你的算法能力飞跃新高度!
【7月更文挑战第17天】并查集,高效解决集合合并查询问题,常用于图的连通性判断。Python实现关键包含查找和合并操作。初始化时,元素各自为集合。查找使用路径压缩优化,合并则可选按秩策略保持平衡。例如,检测无向图环路,遍历边,若并查集发现边两端已在同一集合,则存在环。掌握并查集,提升算法能力,助你在问题解决中一飞冲天!动手实践,成为算法达人!
228 2
|
算法 C++
c++算法学习笔记 (16) 并查集
c++算法学习笔记 (16) 并查集
|
算法
详尽分享经典算法题每日演练——第一题百钱买百鸡
详尽分享经典算法题每日演练——第一题百钱买百鸡
270 0

热门文章

最新文章