从一个 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 |
题目说明N*M的矩阵中可以选出 M!/ N!种组合数组,暴力解法就是根据约束条件枚举这么多组合计算每个组合的第 K 小的数字再更新全局第 K 小的数字。由于所有数字不能同行同列,不能像DFS搜索矩阵那样上下左右递归搜索。但是思路应该是相似的,难点在于没有限制只能访问当前位置的相邻元素,可以是不相邻的,而且必须是和当前搜索路径中已经搜索的所有数字不同行同列。以当前位置搜索下一个元素就不是从当前位置周边展开搜索了,这样代码也不好写,试想我可以搜索周围四个角的位置元素,它们和当前位置不同行不同列,但是未必和之前搜索过的数字也不同行不同列,那么正确的做法是不是从头开始遍历整个矩阵,排除和已经访问过数字同行或同列的数字,其余就是可以访问的数字。可以定义行哈希集合和列哈希集合存储每条路径访问过的数字位置行列索引,这样下次访问别的数字就可以进行位置一一比对筛选不同行不同列数字,回溯的时候再移除最新加入的位置。每次怎么记录第 K 小的数字,每次路径搜索结束时访问的数字序列长度达到 N 时就对序列降序排序取第 K 个元素,用快速排序需要O(N log(N))复杂度,感觉有些浪费,但可以利用快速排序分治的原理快速选择第K大的元素,也可以用优先队列查找第K大(小)元素。这里做法是用优先队列存放固定 K 个元素,用二叉堆实现,堆顶存放最小的数字,每次插入数字都重新调整堆的结构维持堆顶数字最小,这种操作复杂度是O(log n)要比排序更好。
该算法通过深度优先搜索(DFS)结合最小堆(优先队列)的方式,暴力枚举所有可能的 N 个不同行不同列元素组合,从而找到其中第 K 大元素的最小值。以下是详细的算法步骤:
读取输入参数:
N
、列数M
和目标值K
。初始化全局变量:
visited
:二维数组,标记矩阵中每个位置是否已被访问。result
:初始化为无穷大,用于记录最终结果(第 K 大元素的最小值)。rowIndex
, colIndex
:当前处理的元素位置。count
:当前已选择的元素数量。heap
:最小堆,维护当前路径中最大的 K 个元素。rowSet
, colSet
:已选择元素的行号和列号集合,用于确保不同行不同列。result
为当前堆顶元素和result
中的较小值。rowSet
和colSet
排除已选的行列)。K
的最小堆,堆顶元素为当前最大的 K 个元素中最小的一个(即第 K 大元素)。枚举所有可能的起点:
(i, j)
作为起点,初始化堆和访问标记。输出结果:
result
即为第 K 大元素的最小值。function solution() {
let [N, M, K] = readline().split(" ").map(Number);
const mtx = [];
for (let i = 0; i < N; i++) {
mtx[i] = readline().split(" ").map(Number);
}
const visited = Array.from({ length: N }, () => new Array(M).fill(false));
let result = Infinity;
const dfs = function (rowIndex, colIndex, count, heap, rowSet, colSet) {
if (
rowIndex < 0 ||
rowIndex >= N ||
colIndex < 0 ||
colIndex >= M ||
visited[rowIndex][colIndex] ||
count >= N
) {
return;
}
const num = mtx[rowIndex][colIndex];
heap.insert(num);
rowSet.add(rowIndex);
colSet.add(colIndex);
visited[rowIndex][colIndex] = true;
if (count === N - 1) {
result = Math.min(result, heap.peek());
// console.log(heap.toString());
return;
}
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (!rowSet.has(i) && !colSet.has(j) && !visited[i][j]) {
dfs(i, j, count + 1, heap, rowSet, colSet);
}
}
}
};
for (let i = 0; i < N; i++) {
const heap = new MinHeap(K);
// const heap = new PriorityQueue(K);
visited.forEach((item) => item.fill(false));
for (let j = 0; j < M; j++) {
let rowSet = new Set();
let colSet = new Set();
dfs(i, j, 0, heap, rowSet, colSet);
}
}
console.log(result);
}
class MinHeap {
constructor(capacity) {
this.capacity = capacity; // 堆的固定大小
this.heap = [];
}
// 插入元素
insert(num) {
if (this.heap.length < this.capacity) {
this.heap.push(num);
this.heapifyUp();
} else if (num > this.heap[0]) {
this.heap[0] = num;
this.heapifyDown();
}
}
// 从下往上调整堆
heapifyUp() {
let index = this.heap.length - 1;
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] <= this.heap[index]) break;
[this.heap[parentIndex], this.heap[index]] = [
this.heap[index],
this.heap[parentIndex],
];
index = parentIndex;
}
}
// 从上往下调整堆
heapifyDown() {
let index = 0;
while (true) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let smallest = index;
if (
leftChild < this.heap.length &&
this.heap[leftChild] < this.heap[smallest]
) {
smallest = leftChild;
}
if (
rightChild < this.heap.length &&
this.heap[rightChild] < this.heap[smallest]
) {
smallest = rightChild;
}
if (smallest === index) break;
[this.heap[index], this.heap[smallest]] = [
this.heap[smallest],
this.heap[index],
];
index = smallest;
}
}
// 获取堆顶元素(第 K 大的元素)
peek() {
return this.heap[0];
}
toString() {
return this.heap.join("-");
}
}
// js 数组模拟二叉堆(最小堆)
class PriorityQueue {
constructor(capaicity) {
this._list = [];
this._size = 0;
this._capacity = capaicity;
}
insert(num) {
// 如果队列已满且新元素比当前最小值大,则替换最小值
if (this._size >= this._capacity) {
if (num > this._list[0]) {
this._list[0] = num;
this._list.sort((a, b) => a - b); // 重新排序维持最小堆
}
return; // 无论是否替换,队列大小不变
}
// 队列未满时直接插入
this._list.push(num);
this._list.sort((a, b) => a - b); // 保持升序排列
this._size++;
}
peek() {
return this._list[0];
}
toString() {
return this._list.join("-");
}
}
const cases = [
`3 4 2
1 5 6 6
8 3 4 3
6 8 6 3`,
];
let caseIndex = 0;
let lineIndex = 0;
const readline = (function () {
let lines = [];
return function () {
if (lineIndex === 0) {
lines = cases[caseIndex]
.trim()
.split("\n")
.map((line) => line.trim());
}
return lines[lineIndex++];
};
})();
cases.forEach((_, i) => {
caseIndex = i;
lineIndex = 0;
solution();
});
暴力解法(DFS + 最小堆)在理论上可以解决问题,但 无法满足题目给定的规模输入(N≤M≤150)。查阅资料得知可以用二分搜索 + 匈牙利算法解决。在一个矩阵中选取 N 个元素,要求这些元素位于不同的行和列。可以将行号和列号分别看作二分图的两个部分,寻找 N 个互不同行同列的元素,就相当于在这个二分图中找到 N 条边的匹配。假设已经构建了二分图,理论上可以找到多种这样的匹配。但若逐一列出所有匹配并比较其中第 K 大的元素,还是暴力解法,效率低下。转换思路,我们假设已知第 K 大元素的最小值为 kth。那么,矩阵中至多有 N−K+1 个元素值 ≤kth,且这些元素需互不同行同列。因为在这 N 个元素中,有 K−1 个元素比 kth 大,剩下的 N−(K−1)=N−K+1 个元素 ≤kth,这 N−K+1 个元素中包含了 kth(第 K 大值)本身。kth 的大小和二分图的最大匹配数存在正相关关系。当 kth 越小时,满足 ≤kth 的矩阵元素就越少;而 kth 越大,满足 ≤kth 的元素就越多。基于这种关系,我们可以采用二分法来枚举 kth 的值。二分枚举的范围是 1 到矩阵元素的最大值。即使枚举到的 kth 不是矩阵中的元素也无需担心,因为最终我们要找到的第 K 大元素必然是矩阵中的某个值,只有当枚举到矩阵中的某个元素时,才能满足找到足够多 ≤kth 元素的要求。在二分枚举过程中,若当前枚举的 kth 值使得二分图的最大匹配数 ≥N−K+1,则说明 kth 取大了,应将二分的右边界缩小为 kth - 1;反之,若最大匹配数 < N−K+1,则 kth 取小了,需将二分的左边界扩大为 kth + 1。如此反复,即可高效地找到满足条件的第 K 大元素的最小值。
输入处理:读取输入的矩阵维度(N, M, K)和矩阵数据。
二分搜索初始化:确定搜索范围,左边界为矩阵最小值,右边界为矩阵最大值。
二分搜索过程:
构建二分图:对于当前候选值mid,构建一个二分图,其中边表示矩阵中小于等于mid的元素位置。
匈牙利算法:计算二分图的最大匹配数,即最多可以选择多少个不同行和列的小于等于mid的元素。
判定条件:如果最大匹配数至少为N-K+1,说明当前mid可行,记录并尝试更小的mid值;否则,尝试更大的mid值。
输出结果:最终输出的ans即为满足条件的第K大数字的最小值。近似 \(O(N^2 \cdot M \cdot \log C)\)
function solution() {
let input = readline().split(" ");
if (input.length < 3) return;
let [N, M, K] = input.map(Number);
const matrix = [];
for (let i = 0; i < N; i++) {
let row = readline().split(" ");
if (row.length >= M) {
matrix[i] = row.map(Number);
}
}
const buildGraph = function(matrix, mid, N, M) {
const graph = Array.from({ length: N }, () => []);
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (matrix[i][j] <= mid) {
graph[i].push(j);
}
}
}
return graph;
};
const hungarianAlgorithm = function(graph, N, M) {
const matchTo = new Array(M).fill(-1);
let result = 0;
for (let u = 0; u < N; u++) {
const visited = new Array(M).fill(false);
if (dfs(u, graph, matchTo, visited)) {
result++;
}
}
return result;
};
const dfs = function(u, graph, matchTo, visited) {
for (const v of graph[u]) {
if (!visited[v]) {
visited[v] = true;
if (matchTo[v] === -1 || dfs(matchTo[v], graph, matchTo, visited)) {
matchTo[v] = u;
return true;
}
}
}
return false;
};
const flat = matrix.flat();
let left = Math.min(...flat);
let right = Math.max(...flat);
let ans = right;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const graph = buildGraph(matrix, mid, N, M);
const maxMatch = hungarianAlgorithm(graph, N, M);
if (maxMatch >= N - K + 1) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
console.log(ans);
}
const cases = [
`3 4 2
1 5 6 6
8 3 4 3
6 8 6 3`,
];
let caseIndex = 0;
let lineIndex = 0;
const readline = (function () {
let lines = [];
return function () {
if (lineIndex === 0) {
lines = cases[caseIndex]
.trim()
.split("\n")
.map((line) => line.trim());
}
return lines[lineIndex++];
};
})();
cases.forEach((_, i) => {
caseIndex = i;
lineIndex = 0;
solution();
});