1. 项目背景与题目解析
这道来自USACO2012年公开赛的Bookshelf S题目,是典型的动态规划与贪心算法结合的练习题。题目描述了一个农场主需要将N本书按顺序排列到书架上,每本书有宽度W_i和高度H_i。书架的总宽度不能超过L,目标是通过将书分成若干组(每组必须是连续的若干本书),使得各组最高书的高度之和最小。
在实际刷题过程中,我发现这道题完美融合了以下几个关键知识点:
- 动态规划的状态设计与转移方程
- 滑动窗口优化技巧
- 单调队列的应用场景
- 边界条件的处理技巧
2. 核心算法设计思路
2.1 基础动态规划解法
最直观的解法是O(N^2)的动态规划。定义dp[i]表示前i本书的最小高度和,状态转移方程为:
cpp复制dp[i] = min(dp[j] + max_height(j+1,i))
其中 sum_width(j+1,i) <= L
这里需要两层循环:外层遍历i,内层遍历所有可能的j。max_height(j+1,i)表示从第j+1到第i本书中的最大高度,sum_width则是宽度之和。
2.2 滑动窗口优化
观察到书本必须按顺序排列,可以使用滑动窗口维护当前窗口内的书本宽度总和。当加入第i本书导致宽度超过L时,从左侧移出书本直到满足宽度限制。
cpp复制int left = 0, sum_width = 0;
for(int i=1; i<=n; i++){
sum_width += w[i];
while(sum_width > L){
sum_width -= w[++left];
}
// 此时left是最小的满足sum_width <= L的位置
}
2.3 单调队列优化最大值
为了高效获取窗口内的最大高度,可以使用单调递减队列。队列中存储的是可能成为最大值的候选元素的下标,且保持队列头部始终是当前窗口的最大值。
cpp复制deque<int> q;
for(int i=1; i<=n; i++){
// 移除超出窗口的元素
while(!q.empty() && q.front() < left) q.pop_front();
// 维护单调递减性质
while(!q.empty() && h[i] >= h[q.back()]) q.pop_back();
q.push_back(i);
// 当前窗口最大值是h[q.front()]
}
3. 完整代码实现与注释
cpp复制#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
const int MAXN = 1e5+5;
int h[MAXN], w[MAXN], dp[MAXN];
int main() {
int n, L;
cin >> n >> L;
for(int i=1; i<=n; i++) {
cin >> h[i] >> w[i];
}
deque<int> q;
int left = 0, sum_width = 0;
dp[0] = 0;
for(int i=1; i<=n; i++) {
// 维护滑动窗口
sum_width += w[i];
while(sum_width > L) {
sum_width -= w[++left];
}
// 维护单调队列
while(!q.empty() && q.front() < left) q.pop_front();
while(!q.empty() && h[i] >= h[q.back()]) q.pop_back();
q.push_back(i);
// 状态转移
dp[i] = dp[left] + h[q.front()];
for(int j=q.front()+1; j<=i; j++) {
dp[i] = min(dp[i], dp[j] + h[q.front()]);
}
}
cout << dp[n] << endl;
return 0;
}
4. 算法优化与性能分析
4.1 时间复杂度优化
原始DP解法的时间复杂度是O(N^2),经过滑动窗口和单调队列优化后,可以将时间复杂度降低到O(N)。这是因为:
- 每个元素最多进出队列一次,单调队列操作是均摊O(1)
- 滑动窗口的left指针只会向右移动,总共移动N次
4.2 空间复杂度分析
空间复杂度是O(N),主要用于存储:
- 书本的高度和宽度数组
- DP数组
- 单调队列(最多存储N个元素)
4.3 进一步优化思路
可以进一步优化常数因子:
- 使用数组模拟队列而非STL deque
- 预先计算前缀和数组加速宽度求和
- 对于大数据集,可以考虑内存访问优化
5. 常见错误与调试技巧
5.1 边界条件处理
常见错误包括:
- 忘记初始化dp[0] = 0
- 滑动窗口left指针更新不正确
- 单调队列维护时忽略窗口左边界
调试建议:
- 打印中间变量(如left、队列内容、dp值)
- 用小样例手动模拟算法执行过程
5.2 特殊测试用例
需要特别注意的测试情况:
- 所有书本宽度相同
- 单本书的情况
- 所有书本高度相同
- 书本宽度刚好等于L的情况
5.3 性能调优技巧
当遇到TLE时:
- 检查是否有不必要的循环
- 使用更快的输入输出方法(如scanf/printf)
- 减少STL容器的使用
6. 同类题目拓展练习
为了巩固这类问题的解法,推荐练习以下题目:
- LeetCode 239 - 滑动窗口最大值
- POJ 2823 - 滑动窗口最值
- USACO 2016 Jan - Mowing the Field
- Codeforces 940E - Cashback
这些题目都涉及滑动窗口与单调队列的应用,可以帮助深入理解这种优化技巧。
7. 实际应用场景分析
这类算法在实际中有广泛应用:
- 资源分配问题(如带宽分配)
- 时间序列数据分析(如股票价格分析)
- 图像处理中的滑动窗口操作
- 实时系统中的数据流处理
理解这种算法模式,可以解决许多需要维护窗口统计量的问题。
8. 个人解题心得
在解决这道题的过程中,我总结了以下几点经验:
- 先写出基础DP解法,再考虑优化
- 滑动窗口和单调队列是经典组合,需要熟练掌握
- 调试时从小样例入手,逐步扩大规模
- 注意边界条件的处理,特别是下标从0还是1开始
- 对于USACO题目,要特别注意输入输出格式
这道题的价值在于它教会我们如何将看似O(N^2)的问题通过合理优化降到O(N),这种思路在算法竞赛和实际工程中都非常实用。