题目描述
一家矿业公司在长河上有 NNN 个采矿点,每个采矿点位于距离河源 XiX_iXi 的位置,并产出重量为 WiW_iWi 的矿堆。现在需要将这些矿堆重新组合成 KKK 个矿堆(K<NK < NK<N),每个新矿堆必须位于某个原始采矿点。
移动规则如下:
- 驳船只能从上游向下游行驶
- 将重量为 WWW 的矿堆从 XXX 移动到 YYY(Y>XY > XY>X)的成本为 W×(Y−X)W \times (Y - X)W×(Y−X)
- 矿堆可以不被移动
- 总成本是所有移动成本之和
求将 NNN 个初始矿堆重新组合成 KKK 个矿堆的最小总成本。
输入格式
- 第一行:NNN 和 KKK
- 接下来 NNN 行:每行包含 XiX_iXi 和 WiW_iWi
- 采矿点按 XXX 严格升序给出
数据范围
- 1≤K<N≤10001 \leq K < N \leq 10001≤K<N≤1000
- 1≤Xi,Wi≤1061 \leq X_i, W_i \leq 10^61≤Xi,Wi≤106
题目分析
问题本质
这是一个典型的动态规划问题,需要将 NNN 个有序点分成 KKK 段,每段内的所有矿堆都移动到该段的最后一个采矿点,使得总移动成本最小。
关键观察
- 移动方向限制:由于只能向下游移动,最优策略一定是将区间 [l,r][l, r][l,r] 内的所有矿堆都移动到位置 xrx_rxr
- 成本计算:将区间 [l,r][l, r][l,r] 内矿堆移动到 xrx_rxr 的成本为:
cost(l,r)=∑i=lrwi×(xr−xi)=xr×(sumWr−sumWl−1)−(sumXWr−sumXWl−1) \text{cost}(l, r) = \sum_{i=l}^{r} w_i \times (x_r - x_i) = x_r \times (\text{sumW}_r - \text{sumW}_{l-1}) - (\text{sumXW}_r - \text{sumXW}_{l-1}) cost(l,r)=i=l∑rwi×(xr−xi)=xr×(sumWr−sumWl−1)−(sumXWr−sumXWl−1)
其中 sumW\text{sumW}sumW 是重量前缀和,sumXW\text{sumXW}sumXW 是 wi×xiw_i \times x_iwi×xi 的前缀和
动态规划定义
设 dp[i][k]dp[i][k]dp[i][k] 表示将前 iii 个矿堆分成 kkk 段的最小成本。
初始状态:
dp[i][1]=cost(1,i)dp[i][1] = \text{cost}(1, i)dp[i][1]=cost(1,i)
状态转移:
dp[i][k]=minj<i{dp[j][k−1]+cost(j+1,i)}dp[i][k] = \min_{j < i} \{ dp[j][k-1] + \text{cost}(j+1, i) \}dp[i][k]=j<imin{dp[j][k−1]+cost(j+1,i)}
复杂度挑战
直接实现上述 DP\texttt{DP}DP 的时间复杂度为 O(N2K)O(N^2K)O(N2K),在 N,K≤1000N, K \leq 1000N,K≤1000 时会超时,需要优化。
斜率优化
优化思路
将转移方程展开:
dp[i][k]=xi×sumWi−sumXWi+minj<i{−sumWj×xi+(dp[j][k−1]+sumXWj)}
dp[i][k] = x_i \times \text{sumW}_i - \text{sumXW}_i + \min_{j < i} \{ -\text{sumW}_j \times x_i + (dp[j][k-1] + \text{sumXW}_j) \}
dp[i][k]=xi×sumWi−sumXWi+j<imin{−sumWj×xi+(dp[j][k−1]+sumXWj)}
这可以看作对于每个 xix_ixi,在直线集合中寻找最小值:
- 直线斜率:mj=−sumWjm_j = -\text{sumW}_jmj=−sumWj
- 直线截距:bj=dp[j][k−1]+sumXWjb_j = dp[j][k-1] + \text{sumXW}_jbj=dp[j][k−1]+sumXWj
- 查询点:x=xix = x_ix=xi
凸壳优化实现
使用单调队列维护下凸壳:
- 插入新直线时,检查队尾三条直线是否保持凸性
- 查询最小值时,检查队首两条直线的交点,淘汰不优的直线
算法步骤
- 预处理前缀和数组 sumWsumWsumW 和 sumXWsumXWsumXW
- 初始化 dp[i][1]dp[i][1]dp[i][1]
- 对于 k=2k = 2k=2 到 KKK:
- 清空单调队列
- 对于 i=ki = ki=k 到 NNN:
- 将直线 (−sumWi−1,dp[i−1][k−1]+sumXWi−1)(-sumW_{i-1}, dp[i-1][k-1] + sumXW_{i-1})(−sumWi−1,dp[i−1][k−1]+sumXWi−1) 加入队列
- 从队首查询在 xix_ixi 处的最小值
- 更新 dp[i][k]dp[i][k]dp[i][k]
复杂度分析
- 时间复杂度:O(NK)O(NK)O(NK),每个矿堆在每个 kkk 值下最多被插入和弹出队列一次
- 空间复杂度:O(NK)O(NK)O(NK)
参考代码
// Arranging Heaps
// UVa ID: 12524
// Verdict: Accepted
// Submission Date: 2025-11-26
// UVa Run Time: 0.700s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 1005;
const ll INF = 1e18;
ll x[MAXN], w[MAXN];
ll sumW[MAXN], sumXW[MAXN];
ll dp[MAXN][MAXN];
struct Line {
ll m, b;
ll eval(ll x) const { return m * x + b; }
};
bool bad(const Line& l1, const Line& l2, const Line& l3) {
return (l3.b - l1.b) * (l1.m - l2.m) <= (l2.b - l1.b) * (l1.m - l3.m);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
while (cin >> n >> k) {
for (int i = 1; i <= n; i++) {
cin >> x[i] >> w[i];
sumW[i] = sumW[i-1] + w[i];
sumXW[i] = sumXW[i-1] + w[i] * x[i];
}
for (int i = 1; i <= n; i++) {
dp[i][1] = x[i] * sumW[i] - sumXW[i];
for (int j = 2; j <= k; j++)
dp[i][j] = INF;
}
for (int j = 2; j <= k; j++) {
deque<Line> dq;
for (int i = j; i <= n; i++) {
Line newLine = {-sumW[i-1], dp[i-1][j-1] + sumXW[i-1]};
while (dq.size() >= 2 && bad(dq[dq.size()-2], dq.back(), newLine))
dq.pop_back();
dq.push_back(newLine);
while (dq.size() >= 2 && dq[0].eval(x[i]) >= dq[1].eval(x[i]))
dq.pop_front();
dp[i][j] = dq.front().eval(x[i]) + (x[i] * sumW[i] - sumXW[i]);
}
}
cout << dp[n][k] << "\n";
}
return 0;
}
总结
本题展示了动态规划与斜率优化的经典结合:
- 通过问题分析将原问题转化为区间划分问题
- 利用前缀和快速计算区间成本
- 通过斜率优化将平方复杂度降为线性复杂度
斜率优化的关键在于识别出转移方程中的线性结构,并通过维护凸壳来快速查询最小值。这种技巧在解决许多最优化问题中都非常有效。
2294

被折叠的 条评论
为什么被折叠?



