2208 字
11 分钟

四边形不等式优化DP-学习笔记

封面来源

鳴らせ-「七夕が楽(たの)しくなりますように」

例题-邮局 加强版#

题目来源(https://www.luogu.com.cn/problem/P4767)

Part 1: 问题分析与关键的子问题#

首先,我们来分析一下题目。要把 P 个邮局放在 V 个村庄里(或旁边),让每个村庄到最近邮局的距离总和最小。

村庄都在一条直线上,这是一个非常重要的性质。为了方便处理,我们的第一步应该是将所有村庄的坐标排序。设排序后的坐标为 x1,x2,,xVx_1,x_2,\ldots,x_V

一个至关重要的结论是:每个邮局负责的村庄,在排好序的序列中,一定是连续的一段。 为什么呢?如果村庄 ik (i < k) 都去邮局 A,而它们中间的村庄 j (i < j < k) 却去了更远的邮局 B,那还不如让村庄 j 也去邮局 A,总距离一定会更小。

所以,问题转化成了:将 V 个村庄分成 P连续的段,为每一段设立一个邮局,使得总距离和最小。

那么,这就引出了一个子问题:

子问题: 如果我们决定用一个邮局服务第 i 个到第 j 个村庄,这个邮局应该建在哪里,才能使这段村庄的距离和最小?这个最小的距离和是多少?

这是一个经典的“货仓选址”问题。

# 货仓选址

对于一条直线上的点集 xi,xi+1,,xjx_i,x_i+1,\ldots,x_j,要找一个点 y 使得 sumk=k=ijxkysum_k=\sum_{k = i}^{j}∣x_k−y∣ 最小,最优的 y 点应该选在这些点的中位数位置上。

对于已经排好序的村庄 xi,,xjx_i,\ldots,x_j,中位数就是 xmx_m,其中 m=i+j2m=\lfloor\frac{i+j}{2}\rfloor

因此,我们就定义一个代价函数 w(i,j)w(i,j):表示用一个邮局服务第 i 到第 j 个村庄的最小距离和。这个邮局会建在第 i+j2\lfloor\frac{i+j}{2}\rfloor 个村庄的位置上。

好的,现在我们有了代价函数 w(i,j)w(i,j)。我们需要高效地计算出所有可能的 w(i,j)w(i,j)。如果每次都暴力计算,总共需要 O(V3)O(V^3) 的时间,太慢了。我们可以通过预处理坐标的前缀和,在 O(1)O(1) 时间内计算出 w(i,j)w(i,j)

S[k]=suml=1kxlS[k]=sum_{l=1}^{k}x_l 是坐标的前缀和。则 w(i,j)w(i,j) 的值为:

w(i,j)=k=ijxkxm(其中 m=i+j2)w(i, j) = \sum_{k=i}^{j} |x_k - x_m| \quad (\text{其中 } m = \lfloor \frac{i+j}{2} \rfloor)=k=im1(xmxk)+k=m+1j(xkxm)= \sum_{k=i}^{m-1} (x_m - x_k) + \sum_{k=m+1}^{j} (x_k - x_m)=(mi)xm(S[m1]S[i1])+(S[j]S[m])(jm)xm= (m-i)x_m - (S[m-1] - S[i-1]) + (S[j] - S[m]) - (j-m)x_m

这样,预处理前缀和 SS 需要 O(V)O(V),然后便可在 O(1)O(1) 时间内计算出 w(i,j)w(i, j)


Part 2: 建立 DP 模型#

解决了子问题,我们就可以开始构建主 DP 模型了。我们要用 P 个邮局划分 V 个村庄。

第一步:定义状态

我们需要知道“已经处理了多少村庄”和“已经用了多少邮局”。所以,一个很自然的状态定义是: dp[i][j] 表示i 个邮局,覆盖前 j 个村庄的最小总距离和。 我们的目标就是求 dp[P][V]

第二步:状态转移

现在我们来思考如何计算 dp[i][j]。 根据定义,我们要用 i 个邮局覆盖前 j 个村庄。我们可以把第 i 个邮局(也就是最后一个邮局)单独拿出来考虑。假设它负责的村庄区间是 [k+1,j][k+1,j],那么剩下的前 k 个村庄就必须由 i-1 个邮局来负责。

这个分割点 k 可以在 00j1j−1 之间取值(但为了保证有 i - 1 个邮局能放下,k 至少要等于 i - 1)。 于是,状态转移方程就诞生了:

dp[i][j]=mini1k<jdp[i1][k]+w(k+1,j)dp[i][j]=\min_{i−1≤k<j}​{dp[i−1][k]+w(k+1,j)}

这个方程的边界是什么呢?

  • dp[1][j]=w(1,j)dp[1][j] = w(1,j),表示用 1 个邮局覆盖前 j 个村庄的代价。
  • dp[i][i]=0dp[i][i]=0,表示用 i 个邮局覆盖前 i 个村庄,每个村庄都建一个邮局,距离和为 0。

这个 DP 的时间复杂度是 O(PV2)O(P\cdot V^2)i 循环 P 次,j 循环 V 次,k 循环 V 次)。对于 V=3000V=3000, $P=300` 的数据,这个复杂度是无法接受的,必须优化!


Part 3: 四边形不等式登场!#

这个 DP 方程 dp[i][j]=mini1k<jdp[i1][k]+w(k+1,j)dp[i][j]=\min_{i−1≤k<j}​{dp[i−1][k]+w(k+1,j)} 的形式和我们之前讨论的优化形式非常相似!这种形式的 DP 方程,如果代价函数 w(i,j)w(i,j) 满足四边形不等式,就存在决策单调性,可以优化。

四边形不等式定义:对于任意 abcda\le b\le c\le d,有 w(a,c)+w(b,d)w(a,d)+w(b,c)w(a,c)+w(b,d)\le w(a,d)+w(b,c)

一个重要的结论:可以证明(但过程很复杂,我们一般直接作为结论使用),本题中定义的代价函数 w(i,j)w(i,j)(即区间内所有点到中位数的距离和)是满足四边形不等式的。

决策单调性: 如果 ww 满足四边形不等式,那么 DP 的最优决策点就会有单调性。记 s[i][j]s[i][j] 为计算 dp[i][j]dp[i][j] 时,取得最小值的那个 k。那么 s[i][j]s[i][j] 满足:

s[i1][j]s[i][j]s[i][j+1]s[i−1][j]≤s[i][j]≤s[i][j+1]

这个不等式是优化的关键!它告诉我们:

  1. s[i][j]s[i][j+1]s[i][j]\le s[i][j+1]:对于固定的邮局数 i,覆盖的村庄数 j 越多,最优的分割点 k 也会越靠后(或不变)。
  2. s[i1][j]s[i][j]s[i−1][j]\le s[i][j]:对于固定的村庄数 j,使用的邮局数 i 越多,最后一个邮局的分割点 k 也会越靠后(或不变)。这很直观,因为邮局多了,最后一个邮局需要负责的村庄就变少了。

Part 4: O(PV)O(PV) 优化实现#

根据决策单调性 s[i1][j]s[i][j]s[i][j+1]s[i−1][j]\le s[i][j]\le s[i][j+1],我们在计算 dp[i][j]dp[i][j] 时,对最优分割点 k 的搜索范围就可以从 [i1,j1][i−1,j−1] 缩小到 [s[i1][j],s[i][j+1]][s[i−1][j],s[i][j+1]]

我们来看看新的循环结构:

// ... 初始化 dp[1][j] 和 s[1][j]
for (int i = 2; i <= P; ++i) {
// 关键点:j 需要倒序循环
for (int j = V; j >= i; --j) {
int lower_k = s[i - 1][j];
// s[i][j+1] 在 j 的上一次循环 (j+1) 中已经被计算出来了
int upper_k = (j == V) ? (V - 1) : s[i][j + 1];
for (int k = lower_k; k <= upper_k; ++k) {
// ... 更新 dp[i][j] 和 s[i][j]
}
}
}

为什么 j 需要倒序循环? 因为在计算 dp[i][j] 时,我们需要用到 s[i][j+1] 作为 k 的搜索上界。如果我们正序循环 j,那么在算 dp[i][j] 时,s[i][j+1] 还是未知的。而倒序循环 j,在计算 j 时,j+1 的所有信息都已经计算完毕了,就可以放心使用。

复杂度分析 这个优化后的时间复杂度是多少呢? 对于固定的 ijV 循环到 ik 的总循环次数大约是:

j=iV(s[i][j+1]s[i1][j]+1)\sum_{j = i}^{V}​(s[i][j+1]−s[i−1][j]+1)

可以证明(通过上一题讲过的伸缩和),对于每个 ijk 的总循环次数是 O(V)O(V) 的。 外层还有一个 i2P 的循环,所以 DP 部分的总时间复杂度是 O(PV)O(P\cdot V)

加上预处理 w(i,j)w(i,j)O(V2)O(V^2),最终总时间复杂度为 O(V2+PV)O(V^2+PV),对于本题的数据范围来说,已经足够快了!

下面是完整的代码实现,主人可以参考一下~

Part 5: 代码实现#

下面是完整的代码实现:

#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int V, P;
cin >> V >> P;
vector<int> a(V);
for (int i = 0; i < V; i++) {
cin >> a[i];
}
// 计算坐标前缀和,用于快速计算 w(i,j)
vector<i64> pre(V + 1, 0);
for (int i = 0; i < V; i++) {
pre[i + 1] = pre[i] + a[i];
}
// 代价函数 w(i,j):用一个邮局服务区间 [i,j] 的最小距离和
// 邮局建在中位数位置 m = (i+j)/2
auto w = [&](int i, int j) {
int m = (i + j) >> 1; // 中位数位置
// 利用前缀和快速计算距离和
return 1LL * (2 * m - (i + j) + 1) * a[m] - (pre[m + 1] - pre[i]) + (pre[j + 1] - pre[m + 1]);
};
// dp[i][j]: 用 i 个邮局覆盖前 j 个村庄的最小距离和
vector<vector<i64>> dp(P + 1, vector<i64>(V + 1, 1e18));
// s[i][j]: 计算 dp[i][j] 时的最优分割点 k
vector<vector<int>> s(P + 1, vector<int>(V + 2, 0));
// 初始状态:0 个邮局覆盖 0 个村庄,距离为 0
dp[0][0] = 0;
for (int i = 1; i <= P; i++) {
// 边界处理:s[i][V+1] = V,作为最后一个分割点的上界
s[i][V + 1] = V;
// 倒序循环 j,确保计算 dp[i][j] 时 s[i][j+1] 已知
for (int j = V; j >= i; j--) {
// 利用决策单调性缩小搜索范围
int lo = s[i - 1][j]; // 下界:来自 s[i-1][j] ≤ s[i][j]
int hi = s[i][j + 1]; // 上界:来自 s[i][j] ≤ s[i][j+1]
// 在缩小的范围内搜索最优分割点 k
for (int k = lo; k <= min(hi, j - 1); k++) {
// 尝试用前 k 个村庄放 i-1 个邮局,第 i 个邮局负责 [k+1, j]
if (i64 res = dp[i - 1][k] + w(k, j - 1); res < dp[i][j]) {
dp[i][j] = res;
s[i][j] = k; // 记录最优分割点
}
}
}
}
// 输出最终答案:P 个邮局覆盖 V 个村庄的最小距离和
cout << dp[P][V] << "\n";
return 0;
}
四边形不等式优化DP-学习笔记
https://blog.mengh04.top/posts/四边形不等式优化dp-学习笔记/
作者
mengh04
发布于
2025-09-04
许可协议
CC BY-NC-SA 4.0