一、题目描述
给定一个矩阵,包含N*M个整数,和一个包含K个整数的数组现在要求在这个矩阵中找一个宽度最小的子矩阵,要求子矩阵包含数组中所有的整数。
二、输入输出描述
输入描述
- 第一行:两个正整数
N(行数)、M(列数); - 接下来
N行:每行M个整数,表示矩阵内容; - 下一行:正整数
K(目标数组长度); - 最后一行:
K个整数,表示目标数组(可能有重复)。
输出描述
- 一个整数,表示满足要求子矩阵的最小宽度,若找不到,输出-1。
三、示例
|----|---------------------------------|
| 输入 | 2 5 1 2 2 3 1 2 3 2 3 2 3 1 2 3 |
| 输出 | 2 |
| 说明 | 矩阵第0、3列包含了1、2、3,矩阵第3、4列包含了1、2、3 |
|----|---------------------------------|
| 输入 | 2 5 1 2 2 3 1 1 3 2 3 4 3 1 1 4 |
| 输出 | 5 |
| 说明 | 矩阵第1,2,3,4,5列包含了1,1,4 |
四、解题思路
- 核心思想
- 维度降维:将 "二维矩阵的连续列子矩阵" 问题转化为 "一维列窗口" 问题 ------ 先统计每列的数字频次,把 "列" 作为基本单元,窗口的宽度就是子矩阵的宽度;
- 滑动窗口优化:用滑动窗口(双指针)遍历列维度,统计窗口内数字的总频次,当窗口内频次满足目标数组的所有要求时,收缩左边界找最小宽度,时间复杂度从暴力的 O (M²) 优化到 O (M)。
- 问题本质分析
该问题是带频次约束的最小窗口子数组问题的二维扩展,核心特征:
- 约束:子矩阵是 "连续列" 组成的(行全选,列连续),且子矩阵中目标数组的每个数字的出现次数≥目标数组中的次数;
- 优化目标:子矩阵的宽度(列数)最小;
- 关键转化:矩阵的 "连续列子矩阵" 等价于 "列的连续窗口",每列的数字频次是窗口的基本单元,只需统计窗口内的总频次是否满足目标;
- 核心难点:需要同时满足 "数字种类" 和 "每个数字的频次" 要求。
- 核心逻辑
- 列频次预处理:统计每列中每个数字的出现次数,把二维矩阵转化为 "列频次数组",降低后续计算复杂度;
- 目标频次构建:统计目标数组中每个数字的需要次数,明确 "满足条件" 的标准;
- 滑动窗口遍历列 :
- 右边界右移:将当前列的频次加入窗口,更新窗口内总频次,统计 "满足频次要求的数字种类数";
- 满足条件时收缩左边界:尝试缩小窗口宽度,同时更新最小宽度;移除左列频次时,若某数字的频次不再满足目标,更新 "满足种类数";
- 结果判断:遍历完成后,若找到有效窗口则返回最小宽度,否则返回 - 1。
-
步骤拆解
-
输入处理:读取矩阵大小、矩阵内容、目标数组;
-
列频次预处理 :遍历每一列,统计该列中每个数字的出现次数,存储到
colCount数组; -
目标频次构建 :遍历目标数组,统计每个数字需要的总次数,存储到
targetMap; -
滑动窗口遍历列 :
- 初始化:左指针
left=0、匹配种类数match=0、当前窗口频次currMap、最小宽度minWidth=极大值; - 右指针遍历每一列:
- 加入当前列的频次到
currMap,若某数字频次刚满足目标,match+1; - 若
match=目标种类数(窗口满足条件):- 计算当前窗口宽度,更新
minWidth; - 移除左列的频次,若某数字频次不再满足目标,
match-1; - 左指针右移,收缩窗口;
- 计算当前窗口宽度,更新
- 加入当前列的频次到
- 初始化:左指针
-
结果输出 :若
minWidth仍为极大值,返回 - 1;否则返回minWidth。
五、代码实现
java
import java.util.*;
public class MinWidthSubmatrix {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 1. 读取矩阵大小 N(行) M(列)
int N = scanner.nextInt();
int M = scanner.nextInt();
scanner.nextLine(); // 换行符
// 2. 读取矩阵内容
int[][] matrix = new int[N][M];
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
matrix[i][j] = scanner.nextInt();
}
scanner.nextLine();
}
// 3. 读取目标数组参数
int K = scanner.nextInt();
scanner.nextLine();
int[] targetArr = new int[K];
for (int i = 0; i < K; i++) {
targetArr[i] = scanner.nextInt();
}
// 4. 计算最小宽度
int result = findMinWidth(matrix, N, M, targetArr);
System.out.println(result);
scanner.close();
}
/**
* 核心方法:寻找包含目标数组所有数字的最小宽度子矩阵
* @param matrix 输入矩阵
* @param N 矩阵行数
* @param M 矩阵列数
* @param targetArr 目标数组
* @return 最小宽度,无则返回-1
*/
private static int findMinWidth(int[][] matrix, int N, int M, int[] targetArr) {
// 步骤1:统计每列的数字频次 - colCount[j][num] = 第j列num的出现次数
Map<Integer, Integer>[] colCount = new HashMap[M];
for (int j = 0; j < M; j++) {
colCount[j] = new HashMap<>();
for (int i = 0; i < N; i++) {
int num = matrix[i][j];
colCount[j].put(num, colCount[j].getOrDefault(num, 0) + 1);
}
}
// 步骤2:构建目标频次映射(考虑重复)
Map<Integer, Integer> targetMap = new HashMap<>();
for (int num : targetArr) {
targetMap.put(num, targetMap.getOrDefault(num, 0) + 1);
}
int targetType = targetMap.size(); // 目标数字的种类数
// 步骤3:滑动窗口找最小列窗口
int left = 0;
int match = 0; // 已满足频次要求的数字种类数
Map<Integer, Integer> currMap = new HashMap<>();
int minWidth = Integer.MAX_VALUE;
for (int right = 0; right < M; right++) {
// 加入当前列的频次到窗口
Map<Integer, Integer> currCol = colCount[right];
for (Map.Entry<Integer, Integer> entry : currCol.entrySet()) {
int num = entry.getKey();
int cnt = entry.getValue();
// 仅处理目标数组中的数字
if (targetMap.containsKey(num)) {
currMap.put(num, currMap.getOrDefault(num, 0) + cnt);
// 若当前数字的频次刚满足目标,匹配数+1
if (currMap.get(num).equals(targetMap.get(num))) {
match++;
}
}
}
// 尝试收缩左指针,寻找最小窗口
while (match == targetType) {
// 更新最小宽度
int currWidth = right - left + 1;
if (currWidth < minWidth) {
minWidth = currWidth;
}
// 移除左列的频次
Map<Integer, Integer> leftCol = colCount[left];
for (Map.Entry<Integer, Integer> entry : leftCol.entrySet()) {
int num = entry.getKey();
int cnt = entry.getValue();
if (targetMap.containsKey(num)) {
int currCnt = currMap.get(num);
// 若当前数字的频次刚好满足,移除后不再满足,匹配数-1
if (currCnt == targetMap.get(num)) {
match--;
}
currMap.put(num, currCnt - cnt);
// 频次为0时移除,避免冗余
if (currMap.get(num) == 0) {
currMap.remove(num);
}
}
}
left++;
}
}
// 步骤4:结果处理
return minWidth == Integer.MAX_VALUE ? -1 : minWidth;
}
}