1. 题目背景与核心需求解析
洛谷P1103"书本整理"是一道经典的动态规划练习题,主要考察选手对状态转移方程的设计能力和对空间复杂度的优化意识。题目描述的场景是:给定n本书排成一排,每本书有一个高度h_i和宽度w_i,现在需要从中移除恰好k本书,使得剩余书本的总"不整齐度"最小。
所谓"不整齐度"的定义是:对于相邻的两本书i和j,其贡献为|h_i - h_j| × (两本书之间未被移除的书本数 + 1)。这个定义看似简单,但实际计算时需要仔细考虑书本移除对相邻关系的影响。
1.1 题目难点剖析
这道题的核心难点在于:
- 移除书本后相邻关系会动态变化,直接计算不整齐度需要O(n!)的时间复杂度
- 状态设计需要考虑移除书本的数量和位置关系
- 最优子结构的挖掘需要创造性思维
我在第一次尝试时就被这个动态相邻关系搞晕了,后来发现需要将问题转化为"选择n-k本书,保持原始顺序"的视角来处理。
2. 动态规划解法详解
2.1 状态设计思路
经过多次尝试,我发现最合理的状态设计是: dp[i][j] 表示从前i本书中选择了j本书(不移除),且第i本书必选时的最小不整齐度
这样设计的原因是:
- 保证了书本的原始顺序不变
- 明确最后一定选择第i本书,便于计算与前一本书的差值
- j从1开始计数,对应题目中的n-k要求
2.2 状态转移方程推导
状态转移需要考虑前一个被选中的书本位置: dp[i][j] = min(dp[m][j-1] + cost(m,i)) 对于所有m < i
其中cost(m,i) = |h[m] - h[i]| × (i - m - 1)
这个cost函数计算的是:
- 高度差的绝对值
- 两本书之间被移除的书本数(即原始位置差减1)
注意:当j=1时,dp[i][1] = 0,因为只选一本书时不产生不整齐度
2.3 初始化与边界条件
需要特别注意初始化情况:
- dp数组初始化为INF(一个很大的数)
- 对于j=1的情况:dp[i][1] = 0 对所有i
- 最终结果是min(dp[i][n-k]) 对所有i
3. 代码实现与优化技巧
3.1 基础实现版本
#include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; int dp[105][105]; // dp[i][j] 前i本选j本,i必选 int h[105], w[105]; int main() { int n, k; cin >> n >> k; for(int i=1; i<=n; i++) cin >> h[i] >> w[i]; memset(dp, INF, sizeof(dp)); for(int i=1; i<=n; i++) dp[i][1] = 0; for(int j=2; j<=n-k; j++) { for(int i=j; i<=n; i++) { for(int m=j-1; m<i; m++) { int cost = abs(h[i] - h[m]) * (i - m - 1); dp[i][j] = min(dp[i][j], dp[m][j-1] + cost); } } } int res = INF; for(int i=n-k; i<=n; i++) res = min(res, dp[i][n-k]); cout << res << endl; return 0; }3.2 空间优化技巧
观察状态转移可以发现,dp[i][j]只依赖于dp[][j-1],因此可以优化为滚动数组:
int dp[2][105]; // 滚动数组 int now = 0, pre = 1; for(int j=2; j<=n-k; j++) { swap(now, pre); for(int i=j; i<=n; i++) { dp[now][i] = INF; for(int m=j-1; m<i; m++) { int cost = abs(h[i] - h[m]) * (i - m - 1); dp[now][i] = min(dp[now][i], dp[pre][m] + cost); } } }这样空间复杂度从O(n^2)降到了O(n),对于n较大的情况非常有用。
4. 常见错误与调试技巧
4.1 典型错误案例
- 边界条件错误:忘记处理j=1的初始化情况
- 循环顺序错误:j的循环应该放在最外层,因为dp[i][j]依赖于dp[][j-1]
- cost计算错误:误将(i-m)当作被移除的书本数(实际应为i-m-1)
4.2 调试建议
- 打印dp表观察中间结果
- 对小样例手动计算验证
- 特别注意当n-k=1时的特殊情况
重要提示:在洛谷提交时,建议将INF设置为0x3f3f3f3f(约1e9),这个值既足够大,又不会在相加时溢出
5. 算法复杂度分析
时间复杂度:O(n^3)
- 三重循环:j从2到n-k,i从j到n,m从j-1到i-1
空间复杂度:
- 基础版本:O(n^2)
- 优化版本:O(n)
对于题目给定的n≤100,这个复杂度是完全可接受的。如果n增大到1000,则需要考虑更优化的算法。
6. 相似题目推荐
- 洛谷P1091 合唱队形(最长上升子序列变种)
- 洛谷P1280 尼克的任务(区间DP)
- 洛谷P1880 石子合并(区间DP经典题)
- 洛谷P1043 数字游戏(环形区间DP)
这些题目都涉及到状态设计和转移方程的巧妙构建,建议作为延伸练习。我在刷题过程中发现,这类区间处理问题往往需要先固定一个端点,再考虑另一个端点的移动,这与书本整理题的状态设计思路是相通的。