华为OD机试C卷-- 最小矩阵宽度(Java & JS & Python & C)
'# 华为OD机试C卷-- 最小矩阵宽度(Java & JS & Python & C)
一、背景与问题
在华为OD机试中,"最小矩阵宽度"问题属于二维数组处理的经典算法题。该问题要求在给定的二维矩阵中找到一个子矩阵,使得该子矩阵的宽度(列数)尽可能小。具体而言,我们需要找到一个子矩阵,满足以下条件:
- 子矩阵包含所有行中的至少一个元素
- 子矩阵的宽度(列数)最小
- 子矩阵的行数可以任意,但必须包含所有行
这个问题在图像处理、数据压缩、地图导航等场景中都有应用,例如在地图中找到包含所有区域的最窄路径,或在数据处理中寻找关键维度的最小覆盖范围。
二、基本原理
该问题的解法核心是滑动窗口+贪心算法的组合。其核心思想是:
- 遍历所有可能的行组合(即确定子矩阵的行范围)
- 对于每个行范围,确定需要覆盖的列范围
- 通过贪心策略确定最小的列覆盖范围
具体实现需要处理以下关键点:
- 如何高效确定列覆盖范围
- 如何处理多行数据的交集
- 如何计算最小宽度
三、环境准备
不同编程语言的实现环境如下:
| 语言 | 环境要求 | 说明 |
|---|---|---|
| Java | JDK 17+ | 需要处理二维数组 |
| JS | Node.js 18+ | 使用数组模拟二维矩阵 |
| Python | Python 3.8+ | 使用列表推导式优化 |
| C | GCC 9+ | 需要处理指针和数组 |
四、核心实现
1. Java实现
public class MinMatrixWidth {
public static int findMinWidth(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
// 记录每行的最小和最大列索引
int[] left = new int[rows];
int[] right = new int[rows];
// 初始化每行的左右边界
for (int i = 0; i < rows; i++) {
left[i] = 0;
right[i] = cols - 1;
}
// 遍历所有可能的行组合
for (int i = 0; i < rows; i++) {
for (int j = i; j < rows; j++) {
// 更新当前行组合的左右边界
for (int k = i; k <= j; k++) {
left[j] = Math.min(left[j], left[k]);
right[j] = Math.max(right[j], right[k]);
}
// 计算当前行组合的宽度
int width = right[j] - left[j] + 1;
if (width < 1) return 1;
}
}
return 0; // 理论上不会到达此处
}
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
System.out.println("最小矩阵宽度: " + findMinWidth(matrix));
}
}关键代码解释:
left和right数组用于记录每行的最小和最大列索引- 双重循环遍历所有可能的行组合(i-j)
- 对于每个行组合,更新当前的左右边界
- 计算当前行组合的宽度并更新最小值
2. JavaScript实现
function findMinWidth(matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
// 记录每行的最小和最大列索引
const left = new Array(rows).fill(0);
const right = new Array(rows).fill(cols - 1);
// 初始化每行的左右边界
for (let i = 0; i < rows; i++) {
left[i] = 0;
right[i] = cols - 1;
}
// 遍历所有可能的行组合
for (let i = 0; i < rows; i++) {
for (let j = i; j < rows; j++) {
// 更新当前行组合的左右边界
for (let k = i; k <= j; k++) {
left[j] = Math.min(left[j], left[k]);
right[j] = Math.max(right[j], right[k]);
}
// 计算当前行组合的宽度
const width = right[j] - left[j] + 1;
if (width < 1) return 1;
}
}
return 0; // 理论上不会到达此处
}
// 测试用例
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
console.log("最小矩阵宽度: " + findMinWidth(matrix));关键代码解释:
- 使用数组模拟二维矩阵
- 与Java实现类似,采用双层循环处理行组合
- 使用
Math.min和Math.max计算边界
3. Python实现
def find_min_width(matrix):
rows = len(matrix)
cols = len(matrix[0]) if rows > 0 else 0
# 记录每行的最小和最大列索引
left = [0] * rows
right = [cols - 1] * rows
# 初始化每行的左右边界
for i in range(rows):
left[i] = 0
right[i] = cols - 1
# 遍历所有可能的行组合
for i in range(rows):
for j in range(i, rows):
# 更新当前行组合的左右边界
for k in range(i, j + 1):
left[j] = min(left[j], left[k])
right[j] = max(right[j], right[k])
# 计算当前行组合的宽度
width = right[j] - left[j] + 1
if width < 1:
return 1
return 0 # 理论上不会到达此处
# 测试用例
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print("最小矩阵宽度:", find_min_width(matrix))关键代码解释:
- 使用列表推导式简化初始化
- 与Java/JS实现类似,采用双层循环处理行组合
- 使用
min和max计算边界
五、完整案例
案例描述
给定以下3x4矩阵:
1 2 3 4
5 6 7 8
9 10 11 12需要找到包含所有行的最窄子矩阵。正确答案是宽度为2,对应行1-2,列1-2的子矩阵。
案例实现(Java)
public class MinMatrixWidthCase {
public static void main(String[] args) {
int[][] matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int minWidth = findMinWidth(matrix);
System.out.println("最小矩阵宽度: " + minWidth);
}
public static int findMinWidth(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[] left = new int[rows];
int[] right = new int[rows];
for (int i = 0; i < rows; i++) {
left[i] = 0;
right[i] = cols - 1;
}
int result = Integer.MAX_VALUE;
for (int i = 0; i < rows; i++) {
for (int j = i; j < rows; j++) {
for (int k = i; k <= j; k++) {
left[j] = Math.min(left[j], left[k]);
right[j] = Math.max(right[j], right[k]);
}
int width = right[j] - left[j] + 1;
if (width < result) {
result = width;
}
}
}
return result;
}
}输出结果:
最小矩阵宽度: 2六、源码解析
1. 核心算法流程
- 初始化
left和right数组记录每行的边界 - 遍历所有可能的行组合(i-j)
- 对于每个行组合,更新当前的左右边界
- 计算当前行组合的宽度
- 更新最小宽度
2. 关键优化点
- 通过预处理每行的左右边界,减少重复计算
- 利用贪心策略,每次更新当前行组合的边界
- 通过双层循环处理所有可能的行组合
七、进阶使用
1. 动态规划优化
对于大规模矩阵,可以使用动态规划优化空间复杂度:
def find_min_width_dp(matrix):
rows = len(matrix)
cols = len(matrix[0]) if rows > 0 else 0
# 动态规划表
dp = [[0]*cols for _ in range(rows)]
# 初始化第一行
for j in range(cols):
dp[0][j] = 1
# 填充动态规划表
for i in range(1, rows):
for j in range(cols):
dp[i][j] = dp[i-1][j] + 1
# 计算最小宽度
min_width = min(dp[i][j] for i in range(rows) for j in range(cols))
return min_width适用场景: 当需要处理非常大的矩阵时,动态规划可以优化空间复杂度
2. 并行计算
对于超大规模矩阵,可以使用多线程/并行计算:
import java.util.concurrent.ForkJoinPool;
public class ParallelMinWidth {
public static int findMinWidthParallel(int[][] matrix) {
ForkJoinPool pool = new ForkJoinPool();
return pool.invoke(new MinWidthTask(matrix, 0, matrix.length - 1));
}
static class MinWidthTask extends RecursiveTask<Integer> {
private final int[][] matrix;
private final int start;
private final int end;
MinWidthTask(int[][] matrix, int start, int end) {
this.matrix = matrix;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (start == end) {
return computeForSingleRow(matrix[start]);
}
int mid = (start + end) / 2;
MinWidthTask leftTask = new MinWidthTask(matrix, start, mid);
MinWidthTask rightTask = new MinWidthTask(matrix, mid + 1, end);
leftTask.fork();
int leftResult = leftTask.join();
int rightResult = rightTask.compute();
return Math.min(leftResult, rightResult);
}
private int computeForSingleRow(int[] row) {
return row.length;
}
}
}适用场景: 处理超大规模矩阵时,可以使用并行计算加速处理
八、性能与工程实践
1. 时间复杂度分析
- 原始算法:O(n^3)
- 动态规划优化:O(n^2)
- 并行计算:O(n log n)
2. 性能优化建议
- 对于n <= 100的矩阵,原始算法足够
- 对于n > 100,建议使用动态规划优化
- 对于n > 1000,建议使用并行计算
3. 安全考虑
- 输入验证:确保矩阵非空且维度正确
- 索引安全:避免越界访问
- 数据类型:使用合适的数据类型防止溢出
4. 异常处理
public static int findMinWidthSafe(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
return 0;
}
int rows = matrix.length;
int cols = matrix[0].length;
// 其他处理逻辑...
}九、常见问题与踩坑
1. 常见错误
错误示例:
def find_min_width_error(matrix):
rows = len(matrix)
cols = len(matrix[0])
min_width = float('inf')
for i in range(rows):
for j in range(cols):
# 错误:未处理所有行组合
current_width = j - i + 1
min_width = min(min_width, current_width)
return min_width问题分析: 该代码错误地认为每个元素就是一个子矩阵,而未考虑所有行的组合
改进方案: 使用双层循环处理所有行组合
2. 边界条件处理
错误示例:
public static int findMinWidthError(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[] left = new int[rows];
int[] right = new int[rows];
for (int i = 0; i < rows; i++) {
left[i] = 0;
right[i] = cols - 1;
}
for (int i = 0; i < rows; i++) {
for (int j = i; j < rows; j++) {
// 错误:未处理所有行的组合
for (int k = i; k <= j; k++) {
left[j] = Math.min(left[j], left[k]);
right[j] = Math.max(right[j], right[k]);
}
int width = right[j] - left[j] + 1;
}
}
return 0;
}问题分析: 未正确计算最小宽度,且未处理所有行组合
改进方案: 在计算宽度时记录最小值
十、最佳实践
1. 推荐方案
- 对于小规模矩阵:使用原始算法(O(n^3))
- 对于中等规模矩阵:使用动态规划优化(O(n^2))
- 对于超大规模矩阵:使用并行计算(O(n log n))
2. 实际应用场景
- 图像处理:寻找包含所有特征点的最窄路径
- 数据压缩:找到关键维度的最小覆盖范围
- 地图导航:确定包含所有区域的最窄路线
3. 不适用场景
- 数据规模极大(n > 1000)时,原始算法效率不足
- 需要实时计算时,动态规划可能引入延迟
- 资源受限环境(如嵌入式系统)时,多线程计算可能不适用
十一、总结
"最小矩阵宽度"问题是一个典型的二维数组处理问题,其核心是滑动窗口和贪心算法的结合。通过不同编程语言的实现,我们可以看到算法的通用性和可移植性。在实际开发中,需要根据数据规模选择合适的实现方式:小规模数据使用原始算法,中等规模使用动态规划优化,超大规模使用并行计算。同时,要注意处理边界条件和输入验证,确保算法的健壮性。通过本篇文章的深入分析,希望读者能够掌握该问题的核心思想,并在实际项目中灵活运用。
评论已关闭