华为OD (E卷,200分) - 矩阵匹配(Java)

题目描述

从一个 N * M(N ≤ M)的矩阵中选出 N 个数,任意两个数字不能在同一行或同一列,求选出来的 N 个数中第 K 大的数字的最小值是多少。

输入描述

输入矩阵要求:1 ≤ K ≤ N ≤ M ≤ 150

输入格式:

N M K

N*M矩阵

输出描述

N*M 的矩阵中可以选出 M! / N! 种组合数组,每个组合数组种第 K 大的数中的最小值。无需考虑重复数字,直接取字典排序结果即可。

注意:结果是第 K 大的数字的最小值

用例

输入

3 4 2
1 5 6 6
8 3 4 3
6 8 6 3

输出

3

N*M的矩阵中可以选出 M!/ N!种组合数组,每个组合数组种第 K 大的数中的最小值;
上述输入中选出数组组合为:
1,3,6;
1,3,3;
1,4,8;
1,4,3;

上述输入样例中选出的组合数组有24种,最小数组为1,3,3,则第2大的最小值为3

题解

import java.util.*;

public class 矩阵匹配 {
    static int n, m, k;  // n、m、k 分别表示矩阵的行数、列数和要求的第K大数
    static int[][] matrix;  // matrix 用于存储输入的矩阵
    static int[] match;  // match 数组用于存储匹配信息,match[j] = i 表示第j列与第i行匹配
    static boolean[] vis;  // vis 数组用于标记每一列在当前增广路中是否被访问过

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();  // 读取行数
        m = sc.nextInt();  // 读取列数
        k = sc.nextInt();  // 读取k值

        int min = 1, max = Integer.MIN_VALUE;  // 初始化二分查找的上下界
        matrix = new int[n][m];  // 初始化矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                matrix[i][j] = sc.nextInt();  // 读取矩阵元素
                max = Math.max(max, matrix[i][j]);  // 更新矩阵元素的最大值,作为二分查找的上界
            }
        }

        // 二分查找确定第K大的数的最小可能值
        while (min <= max) {
            int mid = (min + max) / 2;  // 取中间值
            if (check(mid)) {
                max = mid - 1;  // 如果当前中间值满足条件,则尝试寻找更小的值
            } else {
                min = mid + 1;  // 如果不满足条件,则尝试寻找更大的值
            }
        }
        System.out.println(min);  // 输出最终结果
    }

    // 检查当前值是否满足条件
    public static boolean check(int currentVal) {
        match = new int[m];  // 初始化匹配数组
        Arrays.fill(match, -1);  // 将所有列初始化为未匹配状态
        vis = new boolean[m];  // 初始化访问标记数组
        int smallerCount = 0;  // 统计满足条件的数量
        for (int i = 0; i < n; i++) {
            Arrays.fill(vis, false);  // 每次搜索前重置访问标记
            if (dfs(i, currentVal)) {
                smallerCount++;  // 如果找到增广路径,则计数增加
            }
        }
        return smallerCount >= n - k + 1;  // 检查是否有足够的小于等于currentVal的数
    }

    // 深度优先搜索寻找增广路径
    public static boolean dfs(int i, int currentVal) {
        for (int j = 0; j < m; j++) {
            // 检查列j是否未被访问过且第i行第j列的值小于等于currentVal
            if (!vis[j] && matrix[i][j] <= currentVal) {
                vis[j] = true;  // 标记列j为已访问
                // 如果列j未匹配或者列j的匹配行可以匹配到其他列
                if (match[j] == -1 || dfs(match[j], currentVal)) {
                    match[j] = i;  // 将列j与行i匹配
                    return true;  // 找到增广路径
                }
            }
        }
        return false;  // 没有找到增广路径
    }

}

算法

匈牙利算法基础

匈牙利算法,可以参考此链接(二分图的最大匹配、完美匹配和匈牙利算法 - Blog - Renfei Song)初步了解并学习匈牙利算法的基本思想和二分图概念

此链接的匈牙利算法代码Java版如下:

import java.util.*;

public class HungarianAlgorithm {
    static int n, m;  // n, m 分别表示左集合和右集合的大小
    static int[][] graph;  // 图的邻接矩阵,graph[i][j] 表示左集合 i 和右集合 j 之间是否有边
    static int[] matchLeft;  // matchLeft[i] 表示左集合 i 对应匹配的右集合的节点,-1 表示没有匹配
    static int[] matchRight;  // matchRight[j] 表示右集合 j 对应匹配的左集合的节点,-1 表示没有匹配
    static boolean[] visited;  // 标记在 DFS 中是否访问过某个节点

    // 匈牙利算法的 DFS 搜索函数
    public static boolean dfs(int u) {
        for (int v = 0; v < m; v++) {
            // 如果 u 和 v 之间有边,且 v 还未被访问过
            if (graph[u][v] == 1 && !visited[v]) {
                visited[v] = true;  // 标记 v 为已访问
                // 如果 v 没有匹配,或者 v 的匹配点 u' 能找到增广路径
                if (matchRight[v] == -1 || dfs(matchRight[v])) {
                    matchLeft[u] = v;  // 将 u 和 v 匹配
                    matchRight[v] = u;  // 将 v 和 u 匹配
                    return true;
                }
            }
        }
        return false;
    }

    // 匈牙利算法的主函数,返回最大匹配的数量
    public static int hungarianAlgorithm() {
        // 匹配数组初始化为 -1
        matchLeft = new int[n];
        matchRight = new int[m];
        Arrays.fill(matchLeft, -1);
        Arrays.fill(matchRight, -1);
        
        int maxMatching = 0;
        
        // 对每一个左集合中的点 u,尝试找到增广路径
        for (int u = 0; u < n; u++) {
            visited = new boolean[m];  // 重置访问标记数组
            if (dfs(u)) {
                maxMatching++;  // 找到增广路径,增加匹配数
            }
        }
        
        return maxMatching;  // 返回最大匹配数
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入左集合的大小 n:");
        n = sc.nextInt();
        System.out.print("请输入右集合的大小 m:");
        m = sc.nextInt();
        
        // 构建邻接矩阵,表示二分图
        graph = new int[n][m];
        System.out.println("请输入图的邻接矩阵(0表示没有边,1表示有边):");
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                graph[i][j] = sc.nextInt();
            }
        }
        
        // 调用匈牙利算法求最大匹配
        int maxMatching = hungarianAlgorithm();
        
        System.out.println("最大匹配数为: " + maxMatching);
        sc.close();
    }
}

为什么是匈牙利算法?

1、不能是同一行同一列,参考二分图的概念,可以把行分为集合U列分为集合V。它们的交点就可以看做它们的边

2、选出来N个数,N恰好就是行数,因此每一行必定会选中其中一个列的数,也就是集合U的节点都会进行匹配,即获取最大匹配的问题

3、根据前两条分析,已经可以断定匈牙利算法,找到第K大的数仅仅是在匈牙利算法上做了小扩展,通过最大权值匹配找出权值前K小的边

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值