1. 蓝桥杯"平台跳跃"问题解析
这道来自蓝桥杯算法竞赛的"平台跳跃"问题,是一个典型的动态规划应用场景。题目描述了一个角色在不同高度的平台间跳跃,需要计算从起点到终点的最小能量消耗。这类问题在实际编程竞赛和算法面试中非常常见,掌握其解法对提升算法能力很有帮助。
1.1 问题场景还原
想象你正在玩一个平台跳跃游戏,有n个不同高度的平台排成一条直线。你从第一个平台出发,每次可以选择:
- 跳到相邻的下一个平台(i→i+1)
- 跳过下一个平台直接跳到下下个平台(i→i+2)
不同跳跃方式的能量消耗计算规则不同:
- 单跳(i→i+1):消耗能量为两平台高度差的绝对值
- 双跳(i→i+2):消耗能量为三倍的两平台高度差的绝对值
我们的目标是找到从第1个平台到第n个平台的最小总能量消耗。
1.2 动态规划思路解析
这个问题非常适合用动态规划(DP)来解决,因为:
- 问题可以分解为子问题(到达每个平台的最小消耗)
- 存在最优子结构(当前最优解依赖于前面子问题的最优解)
- 有重叠子问题(计算dp[i]时会重复用到dp[i-1]和dp[i-2])
DP的核心思想是:记录到达每个状态的最优解,避免重复计算。
2. 算法实现详解
2.1 数据结构设计
首先我们需要合理的数据结构来存储和处理数据:
cpp复制vector<ll> h(n+1); // 平台高度数组,h[1]到h[n]有效
vector<ll> dp(n+1); // DP状态数组,dp[i]表示到达第i个平台的最小能量
这里使用vector而不是普通数组,因为:
- 可以动态确定大小
- 自动管理内存
- 提供边界检查(调试时有用)
使用long long(ll)类型是为了防止大数计算时的溢出问题。
2.2 状态转移方程
这是整个算法的核心部分:
cpp复制dp[i] = min(dp[i-1] + abs(h[i]-h[i-1]), // 从i-1跳过来
dp[i-2] + 3*abs(h[i]-h[i-2])); // 从i-2跳过来
解释:
- 到达平台i的最小能量有两种可能:
- 从i-1平台单跳过来:总能量=到达i-1的能量 + 本次跳跃消耗
- 从i-2平台双跳过来:总能量=到达i-2的能量 + 3倍高度差
我们取这两种情况的最小值作为dp[i]的值。
2.3 边界条件处理
任何DP问题都需要仔细处理边界条件:
cpp复制dp[1] = 0; // 起始平台,能量消耗为0
dp[2] = abs(h[2]-h[1]); // 只能从平台1单跳过来
特别注意:
- dp[0]没有被使用(平台编号从1开始)
- 对于n=1的特殊情况(只有一个平台),直接返回0
- 对于n=2的情况,只有一种跳跃方式
2.4 完整代码实现
cpp复制#include <iostream>
#include <vector>
#include <cmath> // 用于abs函数
using namespace std;
using ll = long long;
int main() {
ll n;
cin >> n;
vector<ll> h(n+1);
for(ll i=1; i<=n; i++) {
cin >> h[i];
}
vector<ll> dp(n+1);
dp[1] = 0;
if(n >= 2) {
dp[2] = abs(h[2]-h[1]);
}
for(ll i=3; i<=n; i++) {
dp[i] = min(dp[i-1] + abs(h[i]-h[i-1]),
dp[i-2] + 3*abs(h[i]-h[i-2]));
}
cout << dp[n] << endl;
return 0;
}
3. 算法优化与注意事项
3.1 空间复杂度优化
当前实现的空间复杂度是O(n),可以优化到O(1):
cpp复制ll prev2 = 0; // dp[i-2]
ll prev1 = abs(h[2]-h[1]); // dp[i-1]
ll current;
for(ll i=3; i<=n; i++) {
current = min(prev1 + abs(h[i]-h[i-1]),
prev2 + 3*abs(h[i]-h[i-2]));
prev2 = prev1;
prev1 = current;
}
优化后只需要常数空间,这在处理大规模数据时很有优势。
3.2 常见错误与调试技巧
- 数组越界:确保平台编号从1开始,处理n=1和n=2的特殊情况
- 整数溢出:使用long long而不是int,特别是当n很大时
- 初始化错误:dp[1]和dp[2]必须正确初始化
- 输入格式错误:确保正确读取n和h数组的值
调试时可以打印dp数组中间结果:
cpp复制for(int i=1; i<=n; i++) {
cout << "dp[" << i << "]=" << dp[i] << endl;
}
3.3 时间复杂度分析
- 时间复杂度:O(n),只需一次遍历
- 空间复杂度:O(n)(可优化到O(1))
- 对于蓝桥杯竞赛规模,这个复杂度完全足够
4. 算法扩展与变种
4.1 不同跳跃规则的变种
如果题目修改跳跃规则,只需调整状态转移方程。例如:
- 如果能跳1、2或3步:
cpp复制dp[i] = min({dp[i-1] + c1, dp[i-2] + c2, dp[i-3] + c3});
- 如果不同跳跃方式消耗不同倍率的高度差
4.2 记录跳跃路径
如果需要输出具体跳跃序列,可以增加一个path数组记录决策:
cpp复制vector<int> path(n+1); // 记录从哪个平台跳过来
for(ll i=3; i<=n; i++) {
if(dp[i-1] + abs(h[i]-h[i-1]) < dp[i-2] + 3*abs(h[i]-h[i-2])) {
dp[i] = dp[i-1] + abs(h[i]-h[i-1]);
path[i] = i-1;
} else {
dp[i] = dp[i-2] + 3*abs(h[i]-h[i-2]);
path[i] = i-2;
}
}
// 反向追踪路径
vector<int> jumps;
for(int i=n; i>1; i=path[i]) {
jumps.push_back(i);
}
jumps.push_back(1);
reverse(jumps.begin(), jumps.end());
4.3 其他类似问题
这类一维DP问题有很多变种:
- 爬楼梯问题(每次1或2步)
- 最小花费爬楼梯
- 打家劫舍问题
- 最大子数组和
它们的共同特点是:当前状态只依赖于前面有限个状态,可以用类似的DP思路解决。
5. 竞赛实战技巧
5.1 输入输出优化
对于大规模数据,可以优化IO速度:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
5.2 测试用例设计
设计测试用例验证程序正确性:
- 小规模数据(n=1,2,3)
- 平台高度全部相同(消耗应为0)
- 平台高度单调递增/递减
- 大规模随机数据
5.3 调试与验证
在竞赛中快速验证算法:
- 先手动计算小样例
- 检查边界条件
- 对比暴力解法结果(如果可能)
6. 个人经验分享
在实际编程竞赛中,这类动态规划问题非常常见。我总结了几点经验:
- 明确状态定义:dp[i]到底表示什么要非常清楚,这是DP的核心
- 画状态转移图:在纸上画出几个平台和转移关系,有助于理解
- 先写转移方程再写代码:先想清楚数学表达,再转化为代码
- 注意初始条件:DP的初始值往往决定整个算法的正确性
- 空间优化放在最后:先写出清晰易懂的版本,正确后再考虑优化
这道题的难点在于理解双跳的3倍能量消耗规则,以及正确处理边界条件。在实际比赛中,建议先处理几个小样例确保理解题意,再着手编写代码。