题目描述
从一个 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小的边