1. 题目背景与核心考察点
2026年得物春招算法岗的这道"爬山"题目,属于典型的动态规划与贪心算法结合类问题。这类题目在互联网大厂的笔试中频繁出现,因为它能有效考察候选人对以下核心能力的掌握程度:
- 动态规划的状态转移方程设计能力
- 贪心策略的合理性与边界条件处理
- 对复杂约束条件的代码实现技巧
- 时间复杂度的分析与优化意识
在实际工作中,类似"爬山"的问题场景广泛存在于路径规划、资源调度、游戏AI等领域。比如电商平台的仓储机器人路径优化、外卖配送的路线选择等,都需要解决带约束条件的最优化问题。
2. 题目描述还原与输入输出分析
根据行业惯例和同类题型特征,我们可以合理还原题目内容:
题目描述:小明计划攀登一座n层台阶的山峰,每层台阶都有一个高度值h[i]。他每次可以选择:
- 向上爬1层,消耗体力|h[i+1]-h[i]|
- 向上爬2层,消耗体力|h[i+2]-h[i]|
但是有特殊限制:
- 不能连续两次选择爬2层(即不能"跳级+跳级")
- 初始在第0层,h[0]=0
求到达第n层的最小总体力消耗。
输入格式:
- 第一行:整数n(1 ≤ n ≤ 1e5)
- 第二行:n个整数表示h[1]到h[n]
输出格式:
- 一个整数表示最小体力值
示例:输入: 5 1 2 3 4 5 输出: 4 解释:0→2→3→5,消耗|3-0| + |3-2| + |5-3| = 3+1+0=4
3. 解题思路与算法设计
3.1 动态规划状态定义
定义dp[i][j]表示到达第i层时,上一次跳跃是j步(j∈{1,2})的最小体力消耗。状态转移方程需要考虑跳跃步数的限制:
如果当前是1步到达i层,则前一步可以是1步或2步: dp[i][1] = min(dp[i-1][1], dp[i-1][2]) + |h[i]-h[i-1]|
如果当前是2步到达i层,则前一步必须是1步: dp[i][2] = dp[i-2][1] + |h[i]-h[i-2]| (i≥2)
3.2 初始化与边界条件
- dp[0][1] = dp[0][2] = 0 (起点无消耗)
- dp[1][1] = |h[1]-h[0]|
- dp[1][2] = INF (不可能一步跳两层到第1层)
3.3 空间优化技巧
由于dp[i]只依赖dp[i-1]和dp[i-2],可以使用滚动数组将空间复杂度从O(n)优化到O(1):
prev1 = 0 # dp[i-1][1] prev2 = 0 # dp[i-1][2] curr1 = 0 # dp[i][1] curr2 = float('inf') # dp[i][2] for i in range(1, n+1): curr1 = min(prev1, prev2) + abs(h[i] - h[i-1]) if i >= 2: curr2 = prev1 + abs(h[i] - h[i-2]) else: curr2 = float('inf') # 滚动更新 prev1, prev2 = curr1, curr24. 多语言实现与性能对比
4.1 Java实现
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] h = new int[n+1]; for (int i = 1; i <= n; i++) { h[i] = sc.nextInt(); } int prev1 = 0, prev2 = 0; int curr1 = 0, curr2 = Integer.MAX_VALUE; for (int i = 1; i <= n; i++) { curr1 = Math.min(prev1, prev2) + Math.abs(h[i] - (i>0 ? h[i-1] : 0)); if (i >= 2) { curr2 = prev1 + Math.abs(h[i] - h[i-2]); } else { curr2 = Integer.MAX_VALUE; } prev1 = curr1; prev2 = curr2; } System.out.println(Math.min(curr1, curr2)); } }Java实现要点:
- 使用Scanner处理大规模输入时要注意性能,实际笔试可能改用BufferedReader
- Integer.MAX_VALUE表示不可达状态
- 注意数组下标从1开始到n的边界处理
4.2 C++实现
#include <iostream> #include <vector> #include <climits> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> h(n+1); for (int i = 1; i <= n; ++i) { cin >> h[i]; } int prev1 = 0, prev2 = 0; int curr1 = 0, curr2 = INT_MAX; for (int i = 1; i <= n; ++i) { curr1 = min(prev1, prev2) + abs(h[i] - (i>0 ? h[i-1] : 0)); if (i >= 2) { curr2 = prev1 + abs(h[i] - h[i-2]); } else { curr2 = INT_MAX; } prev1 = curr1; prev2 = curr2; } cout << min(curr1, curr2) << endl; return 0; }C++优化技巧:
- 使用vector替代原生数组更安全
- INT_MAX需要包含 头文件
- 使用std::min和std::abs避免隐式类型转换
4.3 Python实现
n = int(input()) h = [0] + list(map(int, input().split())) prev1 = prev2 = 0 curr1 = curr2 = float('inf') for i in range(1, n+1): curr1 = min(prev1, prev2) + abs(h[i] - h[i-1]) if i >= 2: curr2 = prev1 + abs(h[i] - h[i-2]) else: curr2 = float('inf') prev1, prev2 = curr1, curr2 print(min(curr1, curr2))Python注意事项:
- 输入处理使用split()更高效
- float('inf')表示无穷大
- 多重赋值语法简化状态更新
5. 测试用例设计与边界验证
5.1 常规测试用例
Case 1: 输入: 5 1 2 3 4 5 输出: 4 解释:0→2→3→5 Case 2: 输入: 4 10 20 30 40 输出: 30 解释:0→2→45.2 边界测试用例
Case 3: (最小输入) 输入: 1 100 输出: 100 Case 4: (连续上升) 输入: 6 1 2 3 4 5 6 输出: 3 解释:0→2→4→6 Case 5: (波动高度) 输入: 7 3 1 4 1 5 9 2 输出: 6 解释:0→2→3→5→75.3 性能测试用例
Case 6: (最大规模) 输入: 100000 [1,2,3,...,100000] 预期: 0 解释:可以一直跳两级6. 复杂度分析与优化空间
时间复杂度:
- 单次遍历O(n),每个状态转移O(1)
- 整体时间复杂度O(n)
空间复杂度:
- 滚动数组优化后O(1)
- 原始h数组需要O(n)空间
进一步优化方向:
- 如果题目允许修改原数组,可以去掉h数组的空间
- 并行计算:对于超大规模n,可以分段计算
- 预处理高度差绝对值,减少重复计算
7. 同类题型扩展与变种
7.1 变种1:增加体力恢复机制
每次休息一层可以恢复k点体力,但休息后下一步必须跳1层。
解法:增加dp[i][3]表示在第i层休息的状态,状态转移更复杂。
7.2 变种2:多维跳跃选择
每次可以选择跳1-3层,但不能连续两次选择相同步数。
解法:状态维度扩展到记录前两步的跳跃选择。
7.3 变种3:带权值的最短路径
每层是一个节点,跳跃消耗是边的权值,求最短路径。
解法:可以转化为Dijkstra算法求解。
在实际面试中,面试官可能会基于候选人的表现逐步增加题目难度。建议在掌握基础解法后,主动思考这些变种问题的解决方案。