1. 水库溃坝填补问题解析
最近在准备华为OD机试时遇到一道很有意思的题目——水库溃坝填补。这道题考察的是对实际问题的抽象建模能力,以及如何运用算法思维解决工程问题。下面我将详细解析这道题的解题思路,并提供多种语言的实现代码。
1.1 题目理解与建模
题目描述的是水库溃坝后的填补场景:
- 两侧坝岩高度相等且坚固
- 坝口用宽度为1的柱子高度图表示(非负整数数组)
- 填补材料是宽度为1、高度不一的木材(非负整数数组)
- 目标是用最优填补策略让溃口面积变为最小
举个例子:
- 坝口数组:[3,0,0]
- 两侧坝岩高度:7
- 原始溃口面积计算:(7-3)+(7-0)+(7-0)=18
- 木材数组:[4,7,4,3,3,5]
- 最优填补后面积可以减小
1.2 解题思路分析
这个问题可以抽象为一个典型的贪心算法问题。核心思路是:
- 计算每个坝口位置的当前高度与坝岩高度的差值(即需要填补的高度)
- 将这些差值按从大到小排序
- 将木材也按从大到小排序
- 用最大的木材填补最大的缺口,以此类推
这样做的原理是:最大的缺口对总面积影响最大,优先用最大的木材填补能最大化减少总面积。
1.3 算法实现步骤
具体实现步骤如下:
- 计算每个坝口位置的缺口高度(坝岩高度-当前高度)
- 对缺口高度数组和木材高度数组分别进行降序排序
- 使用双指针法:
- 一个指针遍历缺口数组
- 一个指针遍历木材数组
- 尽可能用最大的木材填补最大的缺口
- 计算填补后的剩余缺口总面积
1.4 代码实现(C++)
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minDamArea(int damHeight, vector<int>& dam, vector<int>& woods) {
// 计算缺口高度
vector<int> gaps;
for (int h : dam) {
gaps.push_back(damHeight - h);
}
// 排序缺口和木材(降序)
sort(gaps.begin(), gaps.end(), greater<int>());
sort(woods.begin(), woods.end(), greater<int>());
int i = 0, j = 0;
int n = gaps.size(), m = woods.size();
// 使用双指针填补缺口
while (i < n && j < m) {
if (woods[j] >= gaps[i]) {
gaps[i] = 0; // 完全填补
i++;
j++;
} else {
gaps[i] -= woods[j]; // 部分填补
j++;
}
}
// 计算剩余缺口总面积
int total = 0;
for (int gap : gaps) {
total += gap;
}
return total;
}
int main() {
int damHeight = 7;
vector<int> dam = {3, 0, 0};
vector<int> woods = {4, 7, 4, 3, 3, 5};
cout << "最小剩余缺口面积: " << minDamArea(damHeight, dam, woods) << endl;
return 0;
}
1.5 代码实现(Java)
java复制import java.util.Arrays;
import java.util.Collections;
public class DamRepair {
public static int minDamArea(int damHeight, int[] dam, int[] woods) {
// 计算缺口高度
Integer[] gaps = new Integer[dam.length];
for (int i = 0; i < dam.length; i++) {
gaps[i] = damHeight - dam[i];
}
// 排序缺口和木材(降序)
Arrays.sort(gaps, Collections.reverseOrder());
Arrays.sort(woods);
reverseArray(woods);
int i = 0, j = 0;
int n = gaps.length, m = woods.length;
// 使用双指针填补缺口
while (i < n && j < m) {
if (woods[j] >= gaps[i]) {
gaps[i] = 0; // 完全填补
i++;
j++;
} else {
gaps[i] -= woods[j]; // 部分填补
j++;
}
}
// 计算剩余缺口总面积
int total = 0;
for (int gap : gaps) {
total += gap;
}
return total;
}
private static void reverseArray(int[] array) {
int i = 0, j = array.length - 1;
while (i < j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
i++;
j--;
}
}
public static void main(String[] args) {
int damHeight = 7;
int[] dam = {3, 0, 0};
int[] woods = {4, 7, 4, 3, 3, 5};
System.out.println("最小剩余缺口面积: " + minDamArea(damHeight, dam, woods));
}
}
1.6 代码实现(Python)
python复制def min_dam_area(dam_height, dam, woods):
# 计算缺口高度
gaps = [dam_height - h for h in dam]
# 排序缺口和木材(降序)
gaps.sort(reverse=True)
woods.sort(reverse=True)
i = j = 0
n, m = len(gaps), len(woods)
# 使用双指针填补缺口
while i < n and j < m:
if woods[j] >= gaps[i]:
gaps[i] = 0 # 完全填补
i += 1
j += 1
else:
gaps[i] -= woods[j] # 部分填补
j += 1
# 计算剩余缺口总面积
return sum(gaps)
# 测试用例
dam_height = 7
dam = [3, 0, 0]
woods = [4, 7, 4, 3, 3, 5]
print("最小剩余缺口面积:", min_dam_area(dam_height, dam, woods))
1.7 代码实现(C语言)
c复制#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于降序排序
int compare(const void* a, const void* b) {
return (*(int*)b - *(int*)a);
}
int minDamArea(int damHeight, int dam[], int damSize, int woods[], int woodsSize) {
// 计算缺口高度
int* gaps = (int*)malloc(damSize * sizeof(int));
for (int i = 0; i < damSize; i++) {
gaps[i] = damHeight - dam[i];
}
// 排序缺口和木材(降序)
qsort(gaps, damSize, sizeof(int), compare);
qsort(woods, woodsSize, sizeof(int), compare);
int i = 0, j = 0;
// 使用双指针填补缺口
while (i < damSize && j < woodsSize) {
if (woods[j] >= gaps[i]) {
gaps[i] = 0; // 完全填补
i++;
j++;
} else {
gaps[i] -= woods[j]; // 部分填补
j++;
}
}
// 计算剩余缺口总面积
int total = 0;
for (int k = 0; k < damSize; k++) {
total += gaps[k];
}
free(gaps);
return total;
}
int main() {
int damHeight = 7;
int dam[] = {3, 0, 0};
int woods[] = {4, 7, 4, 3, 3, 5};
int result = minDamArea(damHeight, dam, 3, woods, 6);
printf("最小剩余缺口面积: %d\n", result);
return 0;
}
1.8 代码实现(JavaScript)
javascript复制function minDamArea(damHeight, dam, woods) {
// 计算缺口高度
let gaps = dam.map(h => damHeight - h);
// 排序缺口和木材(降序)
gaps.sort((a, b) => b - a);
woods.sort((a, b) => b - a);
let i = 0, j = 0;
const n = gaps.length, m = woods.length;
// 使用双指针填补缺口
while (i < n && j < m) {
if (woods[j] >= gaps[i]) {
gaps[i] = 0; // 完全填补
i++;
j++;
} else {
gaps[i] -= woods[j]; // 部分填补
j++;
}
}
// 计算剩余缺口总面积
return gaps.reduce((sum, gap) => sum + gap, 0);
}
// 测试用例
const damHeight = 7;
const dam = [3, 0, 0];
const woods = [4, 7, 4, 3, 3, 5];
console.log("最小剩余缺口面积:", minDamArea(damHeight, dam, woods));
2. 算法优化与边界条件处理
2.1 算法复杂度分析
当前算法的主要时间消耗在排序步骤:
- 缺口数组排序:O(n log n)
- 木材数组排序:O(m log m)
- 双指针遍历:O(min(n, m))
总体时间复杂度为O(n log n + m log m),空间复杂度为O(n)(存储缺口数组)
2.2 边界条件处理
在实际编码中,需要考虑以下边界条件:
- 坝口数组为空:直接返回0
- 木材数组为空:返回原始缺口总面积
- 坝岩高度小于坝口某些位置高度:题目已说明是非负整数数组,这种情况不应出现
- 木材高度为0:可以跳过,因为无法填补任何缺口
2.3 优化思路
- 提前终止:当所有缺口都被填补后,可以提前退出循环
- 木材预处理:可以先将所有高度为0的木材过滤掉
- 并行处理:对于大规模数据,可以考虑并行排序
优化后的Python实现示例:
python复制def min_dam_area_optimized(dam_height, dam, woods):
if not dam:
return 0
# 计算缺口高度并过滤掉已满足的
gaps = [dam_height - h for h in dam if dam_height > h]
if not gaps:
return 0
# 过滤掉高度为0的木材并排序
usable_woods = [w for w in woods if w > 0]
gaps.sort(reverse=True)
usable_woods.sort(reverse=True)
i = j = 0
n, m = len(gaps), len(usable_woods)
# 使用双指针填补缺口
while i < n and j < m:
if usable_woods[j] >= gaps[i]:
gaps[i] = 0
i += 1
j += 1
else:
gaps[i] -= usable_woods[j]
j += 1
# 提前终止:所有缺口都已填补
if gaps[i] == 0:
i += 1
return sum(gaps)
3. 测试用例设计与验证
3.1 典型测试用例
-
基础用例:
- 输入:damHeight=7, dam=[3,0,0], woods=[4,7,4,3,3,5]
- 预期输出:最小剩余缺口面积
-
木材不足:
- 输入:damHeight=5, dam=[1,2,3], woods=[1,1]
- 预期输出:剩余缺口面积应大于0
-
木材充足:
- 输入:damHeight=10, dam=[2,3,4], woods=[8,7,6,5]
- 预期输出:0(完全填补)
-
空坝口:
- 输入:damHeight=5, dam=[], woods=[1,2,3]
- 预期输出:0
-
无木材:
- 输入:damHeight=5, dam=[1,2,3], woods=[]
- 预期输出:原始缺口面积
3.2 测试代码实现(Python pytest)
python复制import pytest
def test_min_dam_area():
# 基础用例
assert min_dam_area(7, [3,0,0], [4,7,4,3,3,5]) == 0
# 木材不足
assert min_dam_area(5, [1,2,3], [1,1]) == (4+3+2) - (1+1)
# 木材充足
assert min_dam_area(10, [2,3,4], [8,7,6,5]) == 0
# 空坝口
assert min_dam_area(5, [], [1,2,3]) == 0
# 无木材
assert min_dam_area(5, [1,2,3], []) == (4+3+2)
# 边界条件:坝口高度等于坝岩高度
assert min_dam_area(5, [5,5,5], [1,2,3]) == 0
4. 实际应用与扩展思考
4.1 实际工程中的应用
这个问题在实际工程中有很多类似场景:
- 资源分配问题:如何用有限的资源最大程度解决问题
- 任务调度:将高优先级的任务分配给性能最好的机器
- 内存管理:用大小不同的内存块满足各种内存请求
4.2 算法扩展
- 多维填补:如果木材和缺口都有宽度和高度两个维度,问题会变得更复杂
- 动态填补:考虑随时间推移,新的木材不断到达的情况
- 成本考虑:不同木材有不同的使用成本,需要最小化总成本
4.3 类似题目推荐
- 背包问题(Knapsack Problem)
- 任务调度问题(Task Scheduling)
- 区间覆盖问题(Interval Covering)
- 华为OD其他机试题:如"水库蓄水"、"大坝加固"等
5. 解题心得与面试技巧
在解决这类算法问题时,我有以下几点心得:
-
问题抽象能力是关键。要能够将实际工程问题转化为数学模型或算法问题。
-
贪心算法在很多情况下都能提供不错的近似解,虽然不一定是全局最优。
-
边界条件要考虑周全,特别是实际工程问题中各种极端情况。
-
代码实现要注重可读性和健壮性,面试中这比单纯的性能优化更重要。
-
测试用例设计要全面,包括正常情况、边界情况和异常情况。
在华为OD机试中,这类问题通常考察:
- 问题分析和抽象能力
- 基础算法的掌握程度
- 代码实现的质量和规范性
- 边界条件的处理能力
建议在准备过程中:
- 多练习类似的算法问题
- 注重代码风格的规范性
- 养成写注释和测试用例的习惯
- 理解每个算法的时间复杂度和空间复杂度
这道"水库溃坝填补"题目很好地结合了实际工程场景和算法思维,通过多种语言的实现,可以帮助我们更好地理解贪心算法的应用。在实际面试中,能够清晰地解释解题思路,并处理各种边界条件,往往比单纯写出正确代码更重要。