1. 题目背景与需求分析
这道题目来自波兰信息学奥林匹克竞赛(POI 2014)的PTA-Little Bird问题,编号P3572。题目描述了一只小鸟需要从一棵树的第1层跳到第n层,每层都有一个高度值h[i]。小鸟每次最多可以向前跳k层(即从i跳到i+1到i+k),但跳跃时需要消耗体力:如果跳到不比当前层高的层,消耗1点体力;否则消耗(高度差)点体力。
我们的目标是编写C++程序,计算小鸟从第1层跳到第n层所需的最小总体力消耗。输入包括树的高度序列和每次询问的k值,输出是对应k值的最小体力消耗。
2. 算法选择与思路解析
2.1 动态规划基础解法
最直观的解法是动态规划。定义dp[i]为跳到第i层的最小体力消耗,则状态转移方程为:
dp[i] = min(dp[j] + cost(j,i)) for j in [i-k, i-1]
其中cost(j,i) = 1 if h[j] >= h[i] else h[i]-h[j]
这种解法时间复杂度为O(nk),对于n=1e6的数据量来说,当k较大时会超时。
2.2 单调队列优化
观察发现,我们实际上是在滑动窗口中寻找最小值,这提示我们可以使用单调队列来优化。维护一个单调递增队列,队列中存储的是层数索引,且对应的dp值也是递增的。这样队首元素就是当前窗口中的最小值。
具体优化点:
- 队列中存储索引而非值,方便判断是否在窗口内
- 入队时,先移除队尾所有dp值不小于当前元素的索引
- 同时要考虑高度的影响,当dp值相同时,保留高度较大的层
3. 代码实现详解
3.1 数据结构定义
cpp复制#include <iostream>
#include <deque>
using namespace std;
const int MAXN = 1e6 + 5;
int h[MAXN], dp[MAXN];
使用deque实现单调队列,h数组存储每层高度,dp数组存储最小体力消耗。
3.2 单调队列实现
cpp复制void solve(int n, int k) {
deque<int> q;
q.push_back(1);
dp[1] = 0;
for (int i = 2; i <= n; ++i) {
// 移除超出窗口的元素
while (!q.empty() && q.front() < i - k)
q.pop_front();
// 计算dp[i]
int j = q.front();
dp[i] = dp[j] + (h[j] <= h[i] ? 1 : 0);
// 维护单调队列
while (!q.empty()) {
int last = q.back();
if (dp[i] < dp[last] || (dp[i] == dp[last] && h[i] >= h[last]))
q.pop_back();
else
break;
}
q.push_back(i);
}
cout << dp[n] << endl;
}
3.3 输入输出处理
cpp复制int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> h[i];
int q, k;
cin >> q;
while (q--) {
cin >> k;
solve(n, k);
}
return 0;
}
使用快速IO加速输入输出,处理多个查询。
4. 算法正确性证明
4.1 单调队列性质保证
队列始终保持dp值单调递增的性质,且相同dp值时保留高度更大的层。这是因为:
- 对于dp值更小的元素,显然更优
- 当dp值相同时,高度更大的层在后续跳跃中可能减少体力消耗(因为可能不需要额外高度差消耗)
4.2 时间复杂度分析
每个元素最多入队出队一次,因此均摊时间复杂度为O(n)每个查询,总复杂度O(nq),能够通过题目限制。
5. 优化技巧与注意事项
5.1 实现细节
- 初始条件处理:从第1层开始,dp[1]=0
- 队列边界处理:确保不访问无效索引
- 高度比较:注意是"不超过"时消耗1点体力
5.2 常见错误
- 忘记处理多个查询的情况
- 单调队列维护条件写错,特别是dp值相等时的处理
- 数组大小不够导致越界
5.3 性能优化
- 使用C风格数组而非vector,减少常数时间
- 使用快速IO,特别是处理大规模数据时
- 避免不必要的计算和比较
6. 测试用例与调试
6.1 样例输入
code复制5
3 2 5 2 3
2
1 2
6.2 预期输出
code复制4
2
6.3 边界测试
- n=1的情况
- k=1的情况
- 所有高度相同的情况
- 高度严格递增/递减的情况
7. 算法扩展与变种
7.1 不同代价函数
如果题目修改代价计算方式,如:
- 每次跳跃固定消耗c体力
- 高度差平方作为消耗
需要相应调整状态转移方程和队列维护条件
7.2 二维跳跃问题
如果小鸟可以在二维平面上跳跃,问题将变得更加复杂,可能需要更高级的数据结构或算法。
8. 竞赛技巧总结
- 识别滑动窗口最值问题是关键
- 动态规划+单调队列是常见优化手段
- 注意处理dp值相同情况下的次优条件
- 大规模数据下IO优化不可忽视
通过这道题目,我们学习了如何将看似O(nk)的动态规划优化为O(n)的解法,这是竞赛中常见的优化技巧。在实际编码时,要特别注意单调队列的维护条件和边界情况的处理。