1. 水坝漏洞修复算法题解析
今天给大家分享一道我自己设计的算法题,题目背景设定在水坝维修场景,结合了路径规划和时间约束,考验对贪心算法和状态空间搜索的理解。这道题在LeetCode上大概能算Hard难度,适合准备面试的同学练手。
1.1 题目核心机制
题目模拟了一个m×n的水坝网格,上面有k个需要修复的漏洞。每个漏洞有三个关键属性:
- 位置(i,j):表示漏洞在水坝上的坐标
- 初始危险值d₀:漏洞的初始危险程度
- 被淹没时间:由水位上升速度决定
两个失败条件需要特别注意:
- 时间t秒时,所有行i ≥ m-t会被淹没,如果漏洞在被淹没前未被修复则失败
- 每个未修复漏洞的危险值每秒+2,当≥100时失败
维修人员从(0,0)出发,每秒可以移动曼哈顿距离3,到达漏洞位置时可以瞬间修复。
1.2 输入输出规范
输入格式:
code复制m n k
i1 j1 d0_1
i2 j2 d0_2
...
ik jk d0_k
输出要求:
- 如果存在可行解,输出任意一个合法的修复顺序(漏洞编号从1开始)
- 无解时输出-1
数据范围限制:
- 1 ≤ m,n ≤ 50
- 1 ≤ k ≤ 10
2. 解题思路分析
2.1 问题性质判断
这道题属于典型的约束满足问题(CSP),具有以下特点:
- 多个约束条件交织(时间约束、危险值约束)
- 需要找到满足所有约束的动作序列
- 搜索空间随着k增大而指数增长
由于k的范围较小(≤10),我们可以考虑使用回溯法或状态空间搜索来解决问题。
2.2 关键变量计算
对于每个漏洞,我们需要计算两个关键时间点:
-
最晚修复时间(deadline):
- 由水位上升决定:t_deadline = m - i - 1
- 由危险值决定:t_deadline = floor((100 - d₀)/2)
- 取两者较小值
-
从当前位置到漏洞的移动时间:
- 曼哈顿距离:|x1-x2| + |y1-y2|
- 移动时间:ceil(曼哈顿距离/3)
2.3 算法选择
考虑到k的范围较小,我们可以采用以下算法思路:
-
贪心预处理:
- 计算每个漏洞的最晚修复时间
- 按最晚修复时间升序排序
- 这样可以优先处理紧急的漏洞
-
回溯搜索:
- 维护当前时间、当前位置、已修复漏洞集合
- 在每个步骤尝试修复一个未修复的漏洞
- 剪枝条件:
- 当前时间 > 漏洞的最晚修复时间
- 任何未修复漏洞的危险值 ≥100
-
状态压缩:
- 使用位掩码表示已修复的漏洞集合
- 对于k=10,只需要10位二进制数即可表示所有状态
3. 详细实现方案
3.1 数据结构设计
python复制class Hole:
def __init__(self, idx, i, j, d0):
self.idx = idx # 漏洞编号(1-based)
self.i = i # 行坐标
self.j = j # 列坐标
self.d0 = d0 # 初始危险值
self.deadline = min(
(100 - d0) // 2, # 危险值约束
(m - 1) - i # 水位约束
)
def __lt__(self, other):
return self.deadline < other.deadline
3.2 核心算法实现
python复制def solve():
holes = sorted(holes) # 按deadline排序
# 状态:(当前时间, 当前位置, 已修复掩码, 修复顺序)
stack = [(0, (0, 0), 0, [])]
while stack:
time, pos, mask, path = stack.pop()
if mask == (1 << k) - 1: # 所有漏洞已修复
return [h.idx for h in path]
for i in range(k):
if not (mask & (1 << i)):
hole = holes[i]
# 计算移动时间
dist = abs(pos[0]-hole.i) + abs(pos[1]-hole.j)
move_time = (dist + 2) // 3 # 向上取整
arrive_time = time + move_time
# 检查约束
if arrive_time > hole.deadline:
continue # 超过deadline
# 检查所有未修复漏洞的危险值
valid = True
for j in range(k):
if not (mask & (1 << j)) and j != i:
danger = holes[j].d0 + 2 * arrive_time
if danger >= 100:
valid = False
break
if not valid:
continue
# 新状态入栈
new_mask = mask | (1 << i)
new_path = path + [hole]
stack.append((arrive_time, (hole.i, hole.j), new_mask, new_path))
return -1 # 无解
3.3 复杂度分析
- 时间复杂度:O(k!),因为最坏情况下需要尝试所有可能的修复顺序
- 空间复杂度:O(k!),用于存储搜索栈
- 实际运行中由于剪枝的存在,复杂度会低于阶乘级
4. 优化技巧与注意事项
4.1 性能优化建议
-
启发式搜索:
- 使用优先队列,优先扩展剩余时间最紧张的路径
- 评估函数可以综合考虑剩余漏洞的紧急程度
-
预处理剪枝:
- 在开始搜索前,检查是否存在无论如何都会超时的漏洞
- 如果有这样的漏洞,可以直接返回-1
-
记忆化:
- 对于相同的(位置, 已修复掩码)状态,只保留时间最早的
- 这样可以避免重复搜索相同状态
4.2 常见错误与调试技巧
-
时间计算错误:
- 注意移动时间的向上取整计算
- 确保危险值的增长计算准确
-
边界条件处理:
- 当m=1时,所有漏洞都在第0行,需要特殊处理
- 当k=1时,直接检查能否在deadline前到达即可
-
输出格式错误:
- 题目要求输出漏洞编号(1-based),不是数组索引(0-based)
- 确保输出顺序与修复顺序一致
4.3 测试用例设计
提供几个关键测试用例帮助验证代码:
- 基本用例:
code复制3 3 2
1 1 10
2 2 20
可能的解:1 2 或 2 1
- 无解用例:
code复制2 2 2
0 0 98
1 1 0
输出:-1(第一个漏洞初始危险值太高)
- 边界用例:
code复制1 1 1
0 0 0
输出:1(唯一漏洞,可以立即修复)
5. 算法扩展与变种
这道题可以有多种变体,适合进一步思考:
-
移动速度变化:
- 维修人员在不同地形有不同的移动速度
- 需要结合Dijkstra算法计算最短路径
-
多人协作:
- 有多个维修人员同时工作
- 需要协调他们的行动路径
-
动态危险增长:
- 危险值增长速度随时间变化
- 需要更复杂的时间管理
-
部分修复:
- 漏洞可以部分修复,降低危险增长速度
- 增加了问题复杂度
在实际工程中,这类问题常见于任务调度、路径规划等场景。掌握这种约束满足问题的解决方法,对解决实际问题很有帮助。