ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

动态规划与贪心算法在爬山问题中的应用

动态规划与贪心算法在爬山问题中的应用

1. 题目背景与核心考察点

2026年得物春招算法岗的这道"爬山"题目,属于典型的动态规划与贪心算法结合类问题。这类题目在互联网大厂的笔试中频繁出现,因为它能有效考察候选人对以下核心能力的掌握程度:

  • 动态规划的状态转移方程设计能力
  • 贪心策略的合理性与边界条件处理
  • 对复杂约束条件的代码实现技巧
  • 时间复杂度的分析与优化意识

在实际工作中,类似"爬山"的问题场景广泛存在于路径规划、资源调度、游戏AI等领域。比如电商平台的仓储机器人路径优化、外卖配送的路线选择等,都需要解决带约束条件的最优化问题。

2. 题目描述还原与输入输出分析

根据行业惯例和同类题型特征,我们可以合理还原题目内容:

题目描述:小明计划攀登一座n层台阶的山峰,每层台阶都有一个高度值h[i]。他每次可以选择:

  1. 向上爬1层,消耗体力|h[i+1]-h[i]|
  2. 向上爬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, curr2

4. 多语言实现与性能对比

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→4

5.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→7

5.3 性能测试用例

Case 6: (最大规模) 输入: 100000 [1,2,3,...,100000] 预期: 0 解释:可以一直跳两级

6. 复杂度分析与优化空间

时间复杂度:

  • 单次遍历O(n),每个状态转移O(1)
  • 整体时间复杂度O(n)

空间复杂度:

  • 滚动数组优化后O(1)
  • 原始h数组需要O(n)空间

进一步优化方向:

  1. 如果题目允许修改原数组,可以去掉h数组的空间
  2. 并行计算:对于超大规模n,可以分段计算
  3. 预处理高度差绝对值,减少重复计算

7. 同类题型扩展与变种

7.1 变种1:增加体力恢复机制

每次休息一层可以恢复k点体力,但休息后下一步必须跳1层。

解法:增加dp[i][3]表示在第i层休息的状态,状态转移更复杂。

7.2 变种2:多维跳跃选择

每次可以选择跳1-3层,但不能连续两次选择相同步数。

解法:状态维度扩展到记录前两步的跳跃选择。

7.3 变种3:带权值的最短路径

每层是一个节点,跳跃消耗是边的权值,求最短路径。

解法:可以转化为Dijkstra算法求解。

在实际面试中,面试官可能会基于候选人的表现逐步增加题目难度。建议在掌握基础解法后,主动思考这些变种问题的解决方案。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进