首页 > 编程语言 >经典算法思想--并查集

经典算法思想--并查集

时间:2024-10-27 20:21:11浏览次数:5  
标签:parent -- 查集 rank 节点 int 算法 rootX find

前言

		(最近在学习Java,所有函数都是用Java语言来书写的)
  • 前言部分是一些前提储备知识

在并查集(Union-Find)数据结构中,rank(中文称为“秩”)是用来表示树的高度或深度的一种辅助信息。它的主要作用是优化合并操作,以保持并查集的结构尽可能扁平,从而提高查询效率。

秩的具体定义

  1. 秩(Rank)

    • 秩用来衡量一个节点在树中的相对高度。具体来说,秩通常指的是树的“深度”或“高度”。初始时,每一个节点的秩可以设定为 0。若秩突然增加,说明该节点的子树的深度在增加。
  2. 合并时使用

    • 在进行 union 操作时,
      • 若两个集合的根节点的秩不同,我们将根节点秩更小的树连接到秩更大的树上。
      • 当两个根节点的秩相同时,将任意一棵树连接到另一棵树上,并将新根节点的秩值加一。这可以避免树过高。

合并示例

  1. 初始状态
    • 每个元素最初是自己父节点(根节点),并且秩都是 0。
Element:   1   2   3   4
Parent:   [1,  2,  3,  4]  // 表示每个元素都是自己的父节点
Rank:     [0,  0,  0,  0]  // 秩初始化为 0
  1. 调用 union(1, 2)
    • 首先找出元素 1 和 2 的根节点。由于它们各自是自己的根节点,所以 find(1) 返回 1,find(2) 返回 2。
    • 由于根节点不同(1 不是 2),可以将它们合并。
    • 因为它们的秩相等,都为 0,所以可以任意选择一个作为新的根节点,此处选择把 2 的父节点设为 1,并将 1 的秩增加 1。
Union(1, 2):
Element:   1   2
Parent:   [1,  1]   // 2 的父节点指向 1(1成为新的根)
Rank:     [1,  0]   // 将 1 的秩增加 1
  1. 状态更新后的图示
    1
   /
  2
  1. 接着进行 union(2, 3)
    • 查找根节点: find(2) 返回 1(2 的父节点是 1),find(3) 返回 3。
    • 1 和 3 是不同的根节点,可以合并。
    • 由于 1 的秩(1)大于 3 的秩(0),所以将 3 的父节点指向 1。
Union(2, 3):
Element:   1   2   3
Parent:   [1,  1,  1]  // 3 的父节点指向 1
Rank:     [1,  0,  0]  // 秩不变
  1. 最后进行 union(1, 3)
    • 查找根节点: find(1) 返回 1,find(3) 返回 1,所以它们已经在同一个集合中,什么也不做。

总结

在进行 union 操作时,我们首先需要找到两个元素的根节点。如果它们的根节点不同,就可以将它们合并。如果相同,则表示它们已经在同一个集合中。

以上是对 union 操作的正确描述和过程演示,谢谢你的耐心!如果你还有其他问题,请随时问我。


并查集简介

并查集(Union-Find)是一种用于处理不重叠集合的数据结构。它特别适合用于解决有关集合连接、合并和查询的问题。并查集通常包括两个主要操作:

  • Find: 查找元素所属的集合。
  • Union: 合并两个集合。

并查集的主要思想

  • 快速查找:利用树形结构可以快速找到集合的根代表。
  • 树的优化
    • 路径压缩:在查找过程中,将访问的每个节点直接连接到根节点,从而优化树的结构,使得树变平,查找效率更高。
    • 按秩合并:在合并过程中,总是将秩(rank)低的树连接到秩高的树,防止树变得过于高。

路径压缩

路径压缩是在 find 操作中进行的优化。当我们执行查找时,我们将经过的所有节点直接连接到根节点。这减少了树的高度,从而提高随后的查找效率。

路径压缩示例代码
public int find(int[] parent, int index) {
    if (parent[index] != index) {
        // 递归地找到根节点,并在回溯阶段将当前节点直接连接到根节点
        parent[index] = find(parent, parent[index]);
    }
    return parent[index];
}
路径压缩示例说明

假设我们有初始集合 [1, 2, 3, 4, 5],其树结构如下:

1
|
2 - 3 - 4
    |
    5
  • 假设 parent = [0, 1, 1, 2, 3, 3],这意味着 2 和 3 的父节点是 1,5 的父节点是 3。
  • 调用 find(parent, 5) 时,路径为 [5 -> 3 -> 1]
  • 路径压缩将改变 5 的父节点直接连接到 1,结果是 parent = [0, 1, 1, 1, 3, 1]
  • 树在调用 find 后,将如下一步变得更平展:
1
|
2 - 3 - 4 - 5

按秩合并

按秩合并是在 union 操作中进行的优化,目的是尽可能保持树的扁平。所谓“秩”,在这里可以理解为树的高度。

按秩合并示例代码
public void union(int[] parent, int[] rank, int index1, int index2) {
    int root1 = find(parent, index1);
    int root2 = find(parent, index2);

    if (root1 != root2) {
        if (rank[root1] > rank[root2]) {
            parent[root2] = root1; // root2合并到root1上
        } else if (rank[root1] < rank[root2]) {
            parent[root1] = root2; // root1合并到root2上
        } else {
            parent[root2] = root1; // 随意合并并增加其中一个的rank
            rank[root1]++;
        }
    }
}
按秩合并示例说明

假设我们有两个树:

  • 第一棵树的根为 A,秩为 2。
  • 第二棵树的根为 B,秩为 3。

调用 union(parent, rank, A, B) 时:

  • 由于 B 的秩大于 A,A 被合并到 B 上,这样就避免了增加树的高度。
  • 在 rank 相同的情况下,任选一个作为新根,并增加该树的 rank。

结合路径压缩和按秩合并

结合这两个优化策略,在大多数实际应用中,findunion 操作可以接近于常数时间复杂度。这种效率使得并查集在处理大量集合合并和查找操作时极为高效。使用上面的两个优化版本的代码,能够保证树的高度不会过于增长,从而优化操作效率。

并查集的应用

并查集被广泛应用于很多算法与实际问题中,比如:

并查集(Union-Find)数据结构在计算机科学中有着广泛的应用,特别是在处理图相关的问题时。下面我将介绍几个具体的实例问题,并展示如何使用并查集来解决这些问题。

1. 网络连接

问题:判断网络中两个节点是否连通。

实例:假设我们有一个网络系统,每一对节点之间有或没有直接连接。如果两个节点是连通的,则它们之间存在一条直接或间接路径。

代码

public class Network {
    private int[] parent;
    private int[] rank;

    public Network(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i;
            rank[i] = 0;
        }
    }

    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }

    public static void main(String[] args) {
        Network network = new Network(5);
        network.union(0, 1);
        network.union(1, 2);
        System.out.println(network.isConnected(0, 2)); // 输出 true
        System.out.println(network.isConnected(0, 3)); // 输出 false
    }
}

2. 图的连通分量

问题:找出图中的连接组件,即连通分量。

实例:给定一个无向图,找出所有的连通分量。

代码

import java.util.*;

public class Graph {
    private int[] parent;
    private int[] rank;

    public Graph(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i; // 初始化,每个节点的父节点指向自己  
            rank[i] = 0;    // 秩初始化为0  
        }
    }

    // 查找并进行路径压缩  
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 联合操作  
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

    // 计算连通分量的数量  
    public int countComponents() {
        Set<Integer> uniqueRoots = new HashSet<>();
        for (int i = 0; i < parent.length; i++) {
            uniqueRoots.add(find(i));
        }
        return uniqueRoots.size();
    }

    // 主函数用于测试  
    public static void main(String[] args) {
        Graph graph = new Graph(5);
        graph.union(0, 1);
        graph.union(1, 2);
        graph.union(3, 4);
        System.out.println(graph.countComponents()); // 输出 2,表明有两个连通分量  
    }
}

3. 最小生成树算法中的环检测

问题:在 Kruskal 算法中,检测添加的边是否会形成环。

实例:找到一个连通无向图的最小生成树。

代码

import java.util.*;

class Edge implements Comparable<Edge> {
    int src, dest, weight;

    Edge(int src, int dest, int weight) {
        this.src = src;
        this.dest = dest;
        this.weight = weight;
    }

    @Override
    public int compareTo(Edge compareEdge) {
        return this.weight - compareEdge.weight;
    }
}

public class KruskalMST {
    private List<Edge> edges;
    private int vertices;

    public KruskalMST(int vertices) {
        this.vertices = vertices;
        edges = new ArrayList<>();
    }

    public void addEdge(int src, int dest, int weight) {
        edges.add(new Edge(src, dest, weight));
    }

    public List<Edge> findMST() {
        Collections.sort(edges);
        int[] parent = new int[vertices];
        int[] rank = new int[vertices];

        // Initialize parent and rank arrays  
        for (int i = 0; i < vertices; i++) {
            parent[i] = i;
            rank[i] = 0;
        }

        List<Edge> mst = new ArrayList<>();

        // Traverse through all edges  
        for (Edge edge : edges) {
            int rootSrc = find(parent, edge.src);
            int rootDest = find(parent, edge.dest);

            // Check if the selected edge forms a cycle  
            if (rootSrc != rootDest) {
                mst.add(edge);
                union(parent, rank, rootSrc, rootDest);
            }
        }

        return mst;
    }

    private int find(int[] parent, int x) {
        if (parent[x] != x) {
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }

    private void union(int[] parent, int[] rank, int x, int y) {
        int rootX = find(parent, x);
        int rootY = find(parent, y);

        if (rootX != rootY) {
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

    // 主函数用于测试  
    public static void main(String[] args) {
        KruskalMST graph = new KruskalMST(4);
        graph.addEdge(0, 1, 10);
        graph.addEdge(1, 3, 15);
        graph.addEdge(0, 2, 6);
        graph.addEdge(2, 3, 4);

        List<Edge> mst = graph.findMST();
        for (Edge edge : mst) {
            System.out.println(edge.src + " - " + edge.dest + ": " + edge.weight);
        }
        // 输出:  
        // 2 - 3: 4  
        // 0 - 2: 6  
        // 0 - 1: 10  
        // 最小生成树的构建避免了环的形成。  
    }
}

4. 动态连通性问题

问题:处理动态连通性查询和合并操作。

实例:在一种动态环境中运行,始终保持对连通性的跟踪。

代码

public class DynamicConnectivity {
    private int[] parent;
    private int[] rank;

    public DynamicConnectivity(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i; // 初始化每个节点的父节点为自己  
            rank[i] = 0;    // 初始化秩为0  
        }
    }

    // 查找并进行路径压缩  
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 联合操作  
    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
        }
    }

    // 检查两个节点是否在同一连通分量内  
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }

    // 主函数用于测试  
    public static void main(String[] args) {
        DynamicConnectivity dc = new DynamicConnectivity(5);
        dc.union(0, 1);
        dc.union(1, 2);

        System.out.println(dc.isConnected(0, 2)); // 输出 true  
        System.out.println(dc.isConnected(0, 3)); // 输出 false  

        dc.union(2, 3);

        System.out.println(dc.isConnected(0, 3)); // 输出 true  
    }
}

这些实例展示了如何应用并查集解决一些常见的动态连通性问题,以及如何通过高效的合并和查找来提高性能。

5.思考应用

有些问题像动态连通性、社交网络中的朋友圈判断等都可以使用并查集来高效解决。特别是在需要动态地合并集合并频繁地查询彼此是否连通时,并查集是理想的选择。例如:

  • 社交网络:确定任何两个人是否属于同一个社交圈。
  • 电网连通性:判断两座城市是否通过电网连接。

通过这些例子,可以看到并查集以其高效的合并和查询能力,从简单集合操作到复杂图结构都有十分广泛的应用。

标签:parent,--,查集,rank,节点,int,算法,rootX,find
From: https://blog.csdn.net/wxdzuishaui/article/details/143272692

相关文章

  • DRF-Parser解析器组件源码分析和应用
    1.解析器源码分析注意:以下源码为了方便理解已进行简化,只保留了解析器相关的代码#视图函数:classMyView(APIView):defpost(self,request):print(self.request.data)#触发解析流程returnResponse("ok")解析并获取数据的源码分析:获取解析器的......
  • curl wget bond
    curlwgetbondcurlcurl是一个用于与服务器进行数据传输的命令行工具。它支持多种协议,包括HTTP、HTTPS、FTP等。基本用法获取网页内容:curlhttp://example.com下载文件:curl-Ohttp://example.com/file.zip保存文件到指定名称:curl-omyfile.ziphttp://example.co......
  • 【AI探索实践】使用Docker部署ChatGPT Next Web个人智能助手
    【AI探索实践】使用Docker部署ChatGPTNextWeb个人智能助手一、ChatGPTNextWeb介绍1.1ChatGPTNextWeb简介1.2主要特点1.3主要使用场景二、本次实践规划2.1本地环境规划2.2本次实践介绍三、本地环境检查3.1检查Docker服务状态3.2检查Docker版本3.3检查doc......
  • ZZJC新生训练赛第十场题解
    先给出比赛链接:https://vjudge.net/contest/667035下面说一下难度分层:(同一难度下按字典序排序,数字为cf难度分)800:ABEG1100:D1200:C1400:H1500:F原题链接A:https://codeforces.com/contest/1850/problem/AB:https://codeforces.com/contest/1991/problem/......
  • 机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
    1.基础算法常见面试篇1.1过拟合和欠拟合常见面试篇一、过拟合和欠拟合是什么?二、过拟合/高方差(overfiting/highvariance)篇2.1过拟合是什么及检验方法?2.2导致过拟合的原因是什么?2.3过拟合的解决方法是什么?三、欠拟合/高偏差(underfiting/highbias)篇3.......
  • 第二场战役
    这个作业属于哪个课程https://edu.cnblogs.com/campus/zjlg/rjjc作业目标:实现一个命令行文本计数统计程序。能正确统计导入的纯英文txt文本中的字符数,单词数,句子数。具体命令行界面要求举例:命令模式:wc.exe[参数][文件名]wc.exe-cfile.txt统计字符数wc.exe-wfile.tx......
  • aes简单混淆加模拟执行
    github中的示例在gtihub中有一个混淆示例,https://github.com/luck-apple/aesTool,把它clone到本地创建项目新建一个native项目,语言选择Java向MainActivity中添加代码/***AES加密,CBC,PKCS5Padding*/publicstaticnativeStringmethod01(Strin......
  • 软硬件开发面试问题大汇总篇——针对非常规八股问题的提问与应答(代码规范与生态管理)
    软硬件开发,对于编码规范、生态管理等等综合问题的考察尤为重要。阐述下环形缓冲区的用途 环形缓冲区(RingBuffer)是一种固定大小的数据结构,常用于实现数据的流式传输或临时存储。在环形缓冲区中,当到达缓冲区的末尾时,它会回绕到开始部分,从而形成一个“环”。用途总结数......
  • grpc的数据传输格式protobuf你了解吗?
    文章目录前言一、grpc为什么要选择protobuf?二、Varint编码2.1字节序2.2定长编码2.3变长编码2.4有符号数的编码三.EncodingTag例子解析字段嵌套的情况repeated字段注意四:Decoding参考资料总结前言本文档主要讲解protobuf中基础的编码规则。先整体描述protobuf数......
  • 【MySQL】实战篇—应用开发:使用MySQL与编程语言(如Python、Java、PHP等)进行交互
    MySQL是存储和管理数据的强大工具,而编程语言(如Python、Java、PHP等)则用于开发应用程序和处理业务逻辑。将这两者结合起来,可以实现数据的存储、查询、更新和管理,进而构建功能强大的应用程序。2.重要性和实际应用场景在软件开发中,数据库与编程语言的交互至关重要,以下是一些常......