【大厂机试题解法笔记】矩阵匹配

题目

从一个 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)要比排序更好。

基本思路:

  1. 枚举矩阵中每个数字作为起点进行搜索,定义一个count作为dfs函数参数记录访问的数字数量,到K时终止一次搜索路径。定义rowSet和colSet存放行列索引,每次搜索时查询set筛选不同行不同列数字;
  2. 选择下一个不同行不同列且未访问过的数字进行搜索,利用备忘录visited记录访问过的位置避免下次回溯又重复访问,维护一个优先队列(最小堆)存放 K 个数字;
  3. 当搜索序列长度达到 N 时从优先队列中取出堆顶的数字即局部第K大的数字更新全局所有第K大的数字中的最小值。

最小堆的作用:动态维护前 K 大元素

  • 最小堆的堆顶是堆中最小的元素
  • 当需要维护当前最大的 K 个元素时,最小堆可以保证:
    • 堆中始终保存当前已知的最大 K 个元素
    • 堆顶是这 K 个元素中的最小值(即第 K 大元素)。

算法过程

该算法通过深度优先搜索(DFS)结合最小堆(优先队列)的方式,暴力枚举所有可能的 N 个不同行不同列元素组合,从而找到其中第 K 大元素的最小值。以下是详细的算法步骤:

1. 输入处理与初始化

  1. 读取输入参数

    • 读取矩阵的行数N、列数M和目标值K
    • 读取矩阵的每个元素。
  2. 初始化全局变量

    • visited:二维数组,标记矩阵中每个位置是否已被访问。
    • result:初始化为无穷大,用于记录最终结果(第 K 大元素的最小值)。

2. 深度优先搜索(DFS)

递归函数参数
  • rowIndexcolIndex:当前处理的元素位置。
  • count:当前已选择的元素数量。
  • heap:最小堆,维护当前路径中最大的 K 个元素。
  • rowSetcolSet:已选择元素的行号和列号集合,用于确保不同行不同列。
递归终止条件
  • 若当前位置越界、已访问或已选择的元素数量达到 N,终止递归。
处理当前元素
  1. 将当前元素加入堆中,并更新行、列集合和访问标记。
  2. 若已选择 N 个元素:
    • 堆顶元素即为当前路径的第 K 大元素。
    • 更新全局结果result为当前堆顶元素和result中的较小值。
递归遍历剩余元素
  • 遍历矩阵中所有未被选择的行和列(通过rowSetcolSet排除已选的行列)。
  • 对每个符合条件的位置,递归调用 DFS 继续搜索。

3. 堆操作(MinHeap 类)

堆结构
  • 固定大小为K的最小堆,堆顶元素为当前最大的 K 个元素中最小的一个(即第 K 大元素)。
插入操作
  • 若堆未满(元素数量 < K),直接插入并调整堆。
  • 若堆已满且新元素大于堆顶元素,替换堆顶并调整堆。
堆调整
  • heapifyUp():从下往上调整堆,确保父节点小于子节点。
  • heapifyDown():从上往下调整堆,确保父节点小于子节点。

4. 主程序流程

  1. 枚举所有可能的起点

    • 对矩阵中的每个元素(i, j)作为起点,初始化堆和访问标记。
    • 调用 DFS 开始搜索。
  2. 输出结果

    • 遍历所有可能的组合后,result即为第 K 大元素的最小值。

复杂度分析

  • 时间复杂度:\(O(N! * M^N)\),其中 N 为行数,M 为列数。每个路径需要 O (N log K) 维护堆。
  • 空间复杂度:\(O(N + 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 大元素的最小值。

算法过程

  1. 输入处理:读取输入的矩阵维度(N, M, K)和矩阵数据。

  2. 二分搜索初始化:确定搜索范围,左边界为矩阵最小值,右边界为矩阵最大值。

  3. 二分搜索过程

    • 构建二分图:对于当前候选值mid,构建一个二分图,其中边表示矩阵中小于等于mid的元素位置。

    • 匈牙利算法:计算二分图的最大匹配数,即最多可以选择多少个不同行和列的小于等于mid的元素。

    • 判定条件:如果最大匹配数至少为N-K+1,说明当前mid可行,记录并尝试更小的mid值;否则,尝试更大的mid值。

  4. 输出结果:最终输出的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();
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/84161.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?

Pod IP 的本质与特性 Pod IP 的定位 纯端点地址&#xff1a;Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址&#xff08;如 10.244.1.2&#xff09;无特殊名称&#xff1a;在 Kubernetes 中&#xff0c;它通常被称为 “Pod IP” 或 “容器 IP”生命周期&#xff1a;与 Pod …

使用osqp求解简单二次规划问题

文章目录 一、问题描述二、数学推导1. 目标函数处理2. 约束条件处理 三、代码编写 一、问题描述 已知&#xff1a; m i n ( x 1 − 1 ) 2 ( x 2 − 2 ) 2 s . t . 0 ⩽ x 1 ⩽ 1.5 , 1 ⩽ x 2 ⩽ 2.5 min(x_1-1)^2(x_2-2)^2 \qquad s.t. \ \ 0 \leqslant x_1 \leqslant 1.5,…

pe文件结构(TLS)

TLS 什么是TLS? TLS是 Thread Local Storage 的缩写&#xff0c;线程局部存储。主要是为了解决多线程中变量同步的问题 如果需要要一个线程内部的各个函数调用都能访问&#xff0c;但其它线程不能访问的变量&#xff08;被称为static memory local to a thread 线程局部静态变…

Electron简介(附电子书学习资料)

一、什么是Electron&#xff1f; Electron 是一个由 GitHub 开发的 开源框架&#xff0c;允许开发者使用 Web技术&#xff08;HTML、CSS、JavaScript&#xff09; 构建跨平台的桌面应用程序&#xff08;Windows、macOS、Linux&#xff09;。它将 Chromium浏览器内核 和 Node.j…

如何使用DeepSeek帮助自己的工作?(Java开发)

如何使用DeepSeek帮助自己的工作&#xff1f; 作为Java开发者&#xff0c;你可以通过以下方式高效利用DeepSeek提升工作效率&#xff08;附具体操作示例&#xff09;&#xff1a; 一、日常编码加速 1. 代码生成与补全 // 输入需求描述&#xff1a; "生成SpringBoot分页…

Uniapp 二维码生成与解析完整教程

前言 使用Uniapp开发多平台应用&#xff0c;二维码生成采用uQRCode插件&#xff0c;非常nice&#x1f601;&#xff01; Coding 原理 使用canvas绘制 生成二维码数据 绘制到canvas上 使用 <uqrcoderef"uqrcodeRef"canvas-id"qrcode":value"qr…

Vue ⑤-自定义指令 || 插槽

自定义指令 自定义指令&#xff1a;自己定义的指令, 可以封装一些 dom 操作&#xff0c; 扩展额外功能。 全局注册 语法&#xff1a; Vue.directive(指令名, {"inserted" (el) {// 可以对 el 标签&#xff0c;扩展额外功能el.focus()} })局部注册 语法&#xff…

Java HttpClient实现简单网络爬虫

今天我将使用Java的HttpClient&#xff08;在Java 11及以上版本中内置&#xff09;来编写一个入门级的网络爬虫示例。 这个示例将演示如何发送HTTP GET请求&#xff0c;获取响应内容&#xff0c;并处理可能出现的异常。 以下是一个基于Java HttpClient&#xff08;Java 11&…

图表类系列各种样式PPT模版分享

图标图表系列PPT模版&#xff0c;柱状图PPT模版&#xff0c;线状图PPT模版&#xff0c;折线图PPT模版&#xff0c;饼状图PPT模版&#xff0c;雷达图PPT模版&#xff0c;树状图PPT模版 图表类系列各种样式PPT模版分享&#xff1a;图表系列PPT模板https://pan.quark.cn/s/20d40aa…

Sonic EVM L1:沉睡的雄狮已苏醒

Sonic 链 , 是 Fantom 基金会升级后的Layer-1区块链&#xff0c;继承了 Fantom Opera 的高性能特性&#xff0c;并通过全面技术优化成为EVM兼容的高吞吐量公链。 官方网站 : https://www.soniclabs.com 一、Sonic 链概述 1. 为什么从 Fantom 更名为 Sonic Sonic 链是 Fant…

篮球杯软件赛国赛C/C++ 大学 B 组补题

3.gcd 模拟 map<pair<int,int>,int>m; void solve(){int n;cin>>n;forr(i,1,n){int ux,uy,vx,vy;cin>>ux>>uy>>vx>>vy;int dxvx-ux,dyvy-uy;if(dx!0&&dy!0){int gabs(__gcd(dx,dy));dx/g,dy/g;//dxdy中除去公共部分(gcd) 就…

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…

项目部署到Linux上时遇到的错误(Redis,MySQL,无法正确连接,地址占用问题)

Redis无法正确连接 在运行jar包时出现了这样的错误 查询得知问题核心在于Redis连接失败&#xff0c;具体原因是客户端发送了密码认证请求&#xff0c;但Redis服务器未设置密码 1.为Redis设置密码&#xff08;匹配客户端配置&#xff09; 步骤&#xff1a; 1&#xff09;.修…

Linux边缘智能:物联网的终极进化

Linux边缘智能&#xff1a;物联网的终极进化 从数据中心到万物终端的智能革命 引言&#xff1a;边缘计算的范式转变 随着物联网设备的爆炸式增长&#xff0c;传统的云计算架构已无法满足实时性、隐私保护和带宽效率的需求。边缘智能应运而生&#xff0c;将计算能力从云端下沉到…

Linux Shell 中的 dash 符号 “-”

Shell中的-&#xff1a;小符号的大智慧 在Unix/Linux系统中&#xff0c;-符号是一个约定俗成的特殊标记&#xff0c;它表示命令应该使用标准输入或标准输出而非文件。这个看似简单的短横线&#xff0c;完美诠释了Unix"一切皆文件"的设计哲学。 作为标准输入/输出的…

JMeter 实现 MQTT 协议压力测试 !

想象一下&#xff0c;你的智能家居系统连接了上千个设备&#xff0c;传感器数据通过 MQTT 协议飞速传输&#xff0c;但突然服务器崩溃&#xff0c;灯光、空调全失控&#xff01;如何确保你的 MQTT 经纪人能承受高负载&#xff1f;答案是 JMeter&#xff01;通过安装 MQTT 插件&…

CKA考试知识点分享(6)---PriorityClass

CKA 版本&#xff1a;1.32 第六套题是涉及PriorityClass相关。 注意&#xff1a;本文不是题目&#xff0c;只是为了学习相关知识点做的实验。仅供参考 实验目的 创建一套PriorityClass &#xff0c;验证PriorityClass的运作策略。 1 环境准备 创建2个pc&#xff0c;一个为高…

暴力破解篇补充-字典

在皮卡丘靶场的第二期&#xff0c;暴力破解模块中&#xff0c;我相信大家短暂的接触了字典这个概念&#xff0c;字典事实上就是包含了大量弱口令的txt文本文件 所以这篇文章用于分享一些字典&#xff1a;https://wwhc.lanzoue.com/ihdl12ybhbhi&#xff08;弱口令字典&#xff…

关于VS2022中C++导入第三方库的方式

首先&#xff0c;新建一个Cpp项目(控制台项目即可&#xff0c;其他项目也无所谓)&#xff0c;右键点击项目名称(Test1)选择属性或者在VS2022工具栏选择调试标签->属性按钮打开属性页。 注意点: 在开始其他操作前请注意先进行 配置和平台选项框的选择。配置选框选定了是配置…

C++中vector类型的介绍和使用

文章目录 一、vector 类型的简介1.1 基本介绍1.2 常见用法示例1.3 常见成员函数简表 二、vector 数据的插入2.1 push_back() —— 在尾部插入一个元素2.2 emplace_back() —— 在尾部“就地”构造对象2.3 insert() —— 在任意位置插入一个或多个元素2.4 emplace() —— 在任意…