四个示例搞懂并查集(Java)

四个示例搞懂并查集(Java)

并查集简介

并查集适用的问题:元素分组问题

并查集操作:

  1. 合并(Merge),一般用于把两个集合合并,集合用一个树状结构表示,最终汇集到一个根节点;
  2. 查询(Find),查询两个节点是否具有相同的根节点。

并查集的两个优化操作:

  1. 路径压缩:在查询的同时,通过parent[i] = find(parent[i]);进行路径压缩,避免了树结构的重复遍历,直接将节点的父节点化为根节点,提高查询效率。
  2. 秩优化:合并过程中为了使树尽可能平衡,引入秩数组减小根节点左右子树高度差。在开启路径压缩时,秩优化对性能提升有限且会增加编码复杂度,一般不做。

示例1(LeetCode 1319 连通网络的操作次数)

用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。

示例 1:
在这里插入图片描述

输入:n = 4, connections = [[0,1],[0,2],[1,2]]输出:1解释:拔下计算机 1 和 2 之间的线缆,并将它插到计算机 1 和 3 上。
示例 2:
在这里插入图片描述

输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]输出:2
示例 3:
输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2]]输出:-1解释:线缆数量不足。
示例 4:
输入:n = 5, connections = [[0,1],[0,2],[3,4],[2,3]]输出:0
提示:1 <= n <= 10^51 <= connections.length <= min(n*(n-1)/2, 10^5)connections[i].length == 20 <= connections[i][0], connections[i][1] < nconnections[i][0] != connections[i][1]没有重复的连接。两台计算机不会通过多条线缆连接。

思路:

实际上题目只需要在利用并查集合并后判断最后的树的数量即可。若初始布线数不足则直接返回-1,否则返回将各个树连接起来需要的边(树的数量-1)即可。

class Solution {
    int[] parent;
    int[] rank;


    public int makeConnected(int n, int[][] connections) {
        int num = connections.length;
        if (num < n-1) {
            return -1;
        }
        parent = new int[n];
        rank = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
        for (int[] connection : connections) {
            int x = connection[0];
            int y = connection[1];
            if (find(x) != find(y)) {
                union(x, y);
            }
        }
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            set.add(find(i));
        }
        return set.size()-1;
    }


    private int find (int i) {
        if (i == parent[i]) {
            return i;
        }
        parent[i] = find(parent[i]);
        return parent[i];
    }


    private void union (int i, int j) {
        int x = find(i);
        int y = find(j);
        if (x == y) {
            return;
        }
        if (rank[x] > rank[y]) {
            parent[y] = x;
        }else if (rank[y] >= rank[x]) {
            parent[x] = y;
        }
        if (rank[y] == rank[x]) {
            rank[y]++;
        }
    }
}

示例2(acwing 836 合并集合)

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤n,m≤10^5

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes

思路:这题是最常规的并查集的题目,只需用并查集的思想将该合并的进行合并即可。

代码:

import java.util.*;
class Main{
    public static int[] parent;
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        parent = new int[n+1];
        for(int i = 1; i <= n; i++){
            parent[i] = i;
        }
        int m = scan.nextInt();
        List<String> res= new ArrayList<>();
        for(int i = 0; i < m; i++){
            String temp = scan.next();
            int j = scan.nextInt();
            int k = scan.nextInt();
            if("M".equals(temp)){
                merge(j,k);
            }else{
                if(getParent(j)==getParent(k)){
                    res.add("Yes");
                }else{
                    res.add("No");
                }
            }
        }
        for(String s : res){
            System.out.println(s);
        }
    }
    public static int getParent(int i){
        if(i==parent[i]){
            return i;
        }else{
            parent[i] = getParent(parent[i]);
            return parent[i];
        }
    }
    public static void merge(int i, int j){
        int x = getParent(i);
        int y = getParent(j);
        parent[y] = x;
    }
}

示例3(acwing 837 连通块中点的数量)

给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。

现在要进行 m 个操作,操作共有三种:

  1. C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
  2. Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
  3. Q2 a,询问点 a 所在连通块中点的数量;
输入格式

第一行输入整数 n 和 m。

接下来 m 行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No

对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤10^5

输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3

思路:基本思路和示例2一致,不同之处在于维护一个权值数组存储并查集中每个根节点所在树的节点个数即可。更新过程也很简单。

代码:

import java.util.*;
class Main{
    public static int[] parent;
    public static int[] nums;
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        parent = new int[n+1];
        nums = new int[n+1];
        for(int i = 1; i <= n ;i++){
            parent[i] = i;
            nums[i] = 1;
        }
        int m = scan.nextInt();
        List<String> res = new ArrayList<String>();
        for(int i = 0; i < m ; i++){
            String order = scan.next();
            if("C".equals(order)){
                int j = scan.nextInt();
                int k = scan.nextInt();
                merge(j,k);
            }else if("Q1".equals(order)){
                int j = scan.nextInt();
                int k = scan.nextInt();
                if(find(j)==find(k)){
                    res.add("Yes");
                }else{
                    res.add("No");
                }
            }else{
                int j = scan.nextInt();
                res.add(String.valueOf(nums[find(j)]));
            }
        }
        for(String s: res){
            System.out.println(s);
        }
    }
    public static int find(int i){
        if(i==parent[i]){
            return i;
        }else{
            parent[i] = find(parent[i]);
            return parent[i];
        }
    }
    public static void merge(int i, int j){
        int x = find(i);
        int y = find(j);
        if(x!=y){
            parent[y] = x;
            nums[x] += nums[y];
        }
    }
}

示例4(acwing 240 食物链)

动物王国中有三类动物 A,B,C这三类动物的食物链构成了有趣的环形。

A 吃 B,B 吃 C,C 吃 A。

现有 N 个动物,以 1∼N编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N 个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 X 和 Y 是同类。

第二种说法是 2 X Y,表示 X 吃 Y。

此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X 或 Y 比 N 大,就是假话;
  3. 当前的话表示 X 吃 X,就是假话。

你的任务是根据给定的 N 和 K 句话,输出假话的总数。

输入格式

第一行是两个整数 N 和 K,以一个空格分隔。

以下 K 行每行是三个正整数 D,X,Y两数之间用一个空格隔开,其中 D 表示说法的种类。

若 D=1,则表示 X 和 Y 是同类。

若 D=2,则表示 X 吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000
0≤K≤100000

输入样例:
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
输出样例:
3

思路:这题首先要理解清楚题目的意思,题目规定了只有三种生物组成的食物链,也即一条食物链是不会包含4种即以上的生物的。最初就因为没理解清这个条件用了两个并查集分别存储相同的和处在一条食物链里的,而实际上这样是没办法解决此题的。

因为食物链只有三种生物组成且必构成环,所以可以维护一个带权的并查集,对每一个节点存储一个该节点到根节点的距离,可以推导出一条路径上距离相邻的三个节点必构成一个食物链。也即对距离d而言,将其对3取余:

  1. 余1:可以被根节点吃
  2. 余2:可以吃根节点
  3. 余0:与根节点是同类

此外,在维护距离数组d时,要注意:

  1. d数组初始化为0,表示当前节点到父节点的距离
  2. 在find操作中维护d,核心在于d[i] += d[parent[i]]; 仍然是做路径压缩,不同之处在于更新d[i]为到根节点的距离。
  3. order==1,i和j是同类,其根节点分别为x和y,此时合并操作用parent[y] = x即可,但由于i,j同类,有最终(d[i]+d[y]-d[j])%3==0,所以更新d[y] = d[j]-d[i],d[j]后续会在find更新(代码中的merge1)
  4. order==2,i吃j,其根节点分别为x和y,此时合并操作用parent[y] = x即可,但由于i吃j,有最终(d[i]-d[j]+1-d[y])%3==0,所以更新d[y] = d[j]-d[i],d[j]后续会在find更新(代码中的merge2)

答案代码:

import java.util.*;
class Main{
    static int[] parent;
    static int[] d;
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        parent = new int[n+1];
        d = new int[n+1];
        for(int i = 1; i <= n; i++){
            parent[i] = i;
        }
        int res = 0;
        int k = scan.nextInt();
        for(int i = 0; i < k; i++){
            int order = scan.nextInt();
            int x = scan.nextInt();
            int y = scan.nextInt();
            if(x>n||y>n){
                res++;
                continue;
            }
            if(order == 1){
                if(x!=y){
                    if(find(x)!=find(y)){
                        merge1(x, y);
                    }else{
                        if((d[x]-d[y])%3!=0)
                            res++;
                    }
                }
            }else{
                if(x==y){
                    res++;
                }else{
                    if(find(x)!=find(y)){
                        merge2(x, y);
                    }else{
                        if((d[y]-d[x]-1)%3!=0)
                            res++;
                    }
                }
            }
        }
        System.out.println(res);
    }
    public static int find(int i){
        if(i==parent[i]){
            return i;
        }else{
            int temp = find(parent[i]);
            d[i] += d[parent[i]];
            parent[i] = temp;
            return parent[i];
        }
    }
    public static void merge1(int i, int j){
        int x = find(i);
        int y = find(j);
        if(x!=y){
            parent[y] = x;
            d[y]=d[i]-d[j];
        }
    }
    public static void merge2(int i, int j){
        int x = find(i);
        int y = find(j);
        if(x!=y){
            parent[y] = x;
            d[y]=d[i]-d[j]+1;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,可以先确定一下你的项目使用的数据库是哪种?比如MySQL、Oracle、SQL Server等等。这里以MySQL为例来给出代码示例。 首先,你需要在MySQL数据库创建一张表来存储充值记录。下面是创建表的SQL语句: ```sql CREATE TABLE recharge_record ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, recharge_amount DECIMAL(10,2) NOT NULL, recharge_time DATETIME NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 这个表有四个字段,分别是id、user_id、recharge_amount和recharge_time。其id是自增长的主键,user_id是充值用户的ID,recharge_amount是充值金额,recharge_time是充值时间。 接下来,你需要编写Java代码来查询充值记录。下面是一个简单的示例: ```java import java.sql.*; public class RechargeRecordQuery { public static void main(String[] args) { Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 获取数据库连接 String url = "jdbc:mysql://localhost:3306/test"; String username = "root"; String password = "123456"; conn = DriverManager.getConnection(url, username, password); // 编写SQL语句 String sql = "SELECT user_id, recharge_amount, recharge_time FROM recharge_record WHERE user_id = ?"; // 创建PreparedStatement对象 pstmt = conn.prepareStatement(sql); // 设置参数 pstmt.setInt(1, 1001); // 执行查询 rs = pstmt.executeQuery(); // 处理结果集 while (rs.next()) { int userId = rs.getInt("user_id"); double rechargeAmount = rs.getDouble("recharge_amount"); Timestamp rechargeTime = rs.getTimestamp("recharge_time"); System.out.println("用户ID:" + userId); System.out.println("充值金额:" + rechargeAmount); System.out.println("充值时间:" + rechargeTime); System.out.println("--------------------"); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } finally { // 释放资源 try { if (rs != null) { rs.close(); } if (pstmt != null) { pstmt.close(); } if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } } ``` 这个示例代码,我们首先加载了MySQL数据库的驱动,然后获取了数据库连接。接下来,我们编写了一个SQL语句来查询指定用户的充值记录,并使用PreparedStatement对象设置了查询参数。最后,我们执行查询并处理结果集。 需要注意的是,这个示例代码只查询了指定用户的充值记录。如果你需要查询全部的充值记录,只需要把查询条件去掉即可。另外,实际项目可能需要对查询结果进行分页或者按照时间倒序排序等操作,这些操作也可以在SQL语句实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值