1. 问题背景与需求分析
最近在刷算法题时遇到一个有趣的"攀登者"问题,题目描述了一位登山爱好者在地图上寻找可攀登山峰的场景。这个问题的核心在于计算登山者在有限体力条件下,能够安全攀登并返回地面的山峰数量。
1.1 问题场景建模
地图被表示为一个一维数组,其中:
- 数组索引代表水平位置
- 数组元素值代表该位置的海拔高度
- 高度为0的位置代表地面
例如地图数组 [0,1,2,4,3,1,0,0,1,2,3,1,2,1,0] 可以可视化为:
code复制高度:
4 | *
3 | * * *
2 | * * * *
1 |* * *
0*-----------*---* (地面)
1.2 体力消耗规则
登山时的体力消耗遵循特定规则:
- 上山:消耗相邻高度差×2的体力
- 下山:消耗相邻高度差×1的体力
- 平地移动:不消耗体力
例如从高度0到1消耗2体力,从1到2消耗4体力,从2到1则消耗1体力。
1.3 问题核心需求
给定地图数组和登山者体力值,需要计算出:
- 地图中有多少座山峰(局部最高点)
- 对于每座山峰,是否存在一条往返路径使得总体力消耗小于登山者体力
- 返回满足条件的山峰数量
2. 解题思路与算法设计
2.1 山峰识别方法
一个位置i是山峰的条件:
height[i] > height[i-1](比左侧高)height[i] > height[i+1](比右侧高)或i是数组末尾
边界情况处理:
- 数组起始位置不可能是山峰
- 高度为0的位置是地面,不是山峰
2.2 体力消耗计算优化
初始思路是分别计算上山和下山消耗:
python复制up_cost += diff * 2 # 上山
down_cost += diff * 1 # 下山
但观察发现:
- 上山时的上坡路段(diff>0)在下山时会变成下坡
- 上山时的下坡路段(diff<0)在下山时会变成上坡
因此可以合并计算:
python复制total_cost += abs(diff) * 3 # 每个高度差贡献3倍体力消耗
2.3 双向遍历的必要性
仅单向遍历会遗漏更优路径。例如:
code复制高度图:
3 | * *
2 | * * * *
1 |* *
0*-*-*-*-*- (地面)
从左侧攀登位置3的山峰可能需要绕过高点,而从右侧可能路径更优。因此需要:
- 正向遍历一次
- 反向遍历一次(数组逆序后同样处理)
2.4 算法流程
- 初始化可攀登山峰集合
- 正向处理数组:
- 找到第一个地面位置作为起点
- 遍历计算到每个山峰的体力消耗
- 记录满足条件的山峰
- 反向处理数组(同样逻辑)
- 返回集合大小
3. 代码实现与解析
3.1 JavaScript实现
javascript复制function getResult(heights, strength) {
const idxs = new Set();
// 正向处理
climb(heights, strength, idxs, true);
// 反向处理
climb([...heights].reverse(), strength, idxs, false);
return idxs.size;
}
function climb(heights, strength, idxs, direction) {
let j = 0;
while (j < heights.length && heights[j] !== 0) j++;
let cost = 0;
for (let i = j + 1; i < heights.length; i++) {
if (heights[i] === 0) {
cost = 0;
continue;
}
const diff = heights[i] - heights[i - 1];
if (diff > 0) {
cost += diff * 3;
// 检查是否为山峰
if (i + 1 >= heights.length || heights[i] > heights[i + 1]) {
if (cost < strength) {
const idx = direction ? i : heights.length - i - 1;
idxs.add(idx);
}
}
} else if (diff < 0) {
cost -= diff * 3;
}
}
}
3.2 Java实现关键点
java复制public static void climb(int[] heights, int strength, HashSet<Integer> idxs, boolean direction) {
int j = 0;
while (j < heights.length && heights[j] != 0) j++;
int cost = 0;
for (int i = j + 1; i < heights.length; i++) {
if (heights[i] == 0) {
cost = 0;
continue;
}
int diff = heights[i] - heights[i - 1];
if (diff > 0) {
cost += diff * 3;
if (i + 1 >= heights.length || heights[i] > heights[i + 1]) {
if (cost < strength) {
int idx = direction ? i : heights.length - i - 1;
idxs.add(idx);
}
}
} else if (diff < 0) {
cost -= diff * 3;
}
}
}
3.3 Python实现技巧
python复制def climb(idxs, direction):
j = 0
while j < len(heights) and heights[j] != 0:
j += 1
cost = 0
for i in range(j + 1, len(heights)):
if heights[i] == 0:
cost = 0
continue
diff = heights[i] - heights[i - 1]
if diff > 0:
cost += diff * 3
if i + 1 >= len(heights) or heights[i] > heights[i + 1]:
if cost < strength:
idx = i if direction else len(heights) - i - 1
idxs.add(idx)
3.4 C语言实现注意事项
c复制void climb(const int heights[], int heights_size, int strength, int direction) {
int j = 0;
while (j < heights_size && heights[j] != 0) j++;
int cost = 0;
for (int i = j + 1; i < heights_size; i++) {
if (heights[i] == 0) {
cost = 0;
continue;
}
int diff = heights[i] - heights[i - 1];
if (diff > 0) {
cost += diff * 3;
if (i + 1 >= heights_size || heights[i] > heights[i + 1]) {
if (cost < strength) {
int idx = direction ? i : heights_size - i - 1;
if(!canClimb[idx]) {
canClimb[idx] = 1;
canClimb_count++;
}
}
}
} else if (diff < 0) {
cost -= diff * 3;
}
}
}
4. 复杂度分析与优化
4.1 时间复杂度
- 正向遍历:O(n)
- 反向遍历:O(n)
- 总体:O(n)
其中n为数组长度。两次线性遍历,效率较高。
4.2 空间复杂度
- 使用集合存储结果:最坏O(n)
- 其他变量:常数空间
- 总体:O(n)
4.3 可能的优化方向
- 并行处理:正向和反向遍历可以并行执行
- 提前终止:当剩余体力不足以攀登更高山峰时可提前终止
- 记忆化:记录中间结果避免重复计算
5. 边界条件与测试案例
5.1 典型测试用例
python复制# 用例1
输入: [0,1,4,3,1,0,0,1,2,3,1,2,1,0], 13
输出: 3
# 用例2
输入: [1,4,3], 999
输出: 0 # 无地面位置
# 用例3
输入: [0,1,0,2,0,3,0], 5
输出: 1 # 只能攀登高度2的山峰
5.2 特殊边界情况
- 全地面数组:
[0,0,0]→ 输出0 - 单峰数组:
[0,1,0]→ 输出1 - 无地面数组:
[1,2,1]→ 输出0 - 平顶山峰:
[0,1,1,0]→ 输出0(非严格山峰)
6. 实际应用与扩展
6.1 实际问题映射
这类算法可以应用于:
- 路径规划中的能量消耗计算
- 游戏中的角色体力管理系统
- 机器人导航中的功耗评估
6.2 算法扩展方向
- 二维地图版本:将一维数组扩展为二维矩阵
- 多登山者协作:考虑团队协作攀登
- 动态体力恢复:引入体力恢复机制
- 不同地形系数:岩石、雪地等不同地形消耗系数不同
在实现这类算法时,关键是要理解体力消耗的计算方式以及双向遍历的必要性。实际编码中,我发现在处理数组逆序时的索引转换容易出错,需要特别注意。另外,对于山峰的判定条件要严格遵循定义,避免将平顶或非极值点误判为山峰。