村庄排序后最优解每段连续,单段代价由中位数给出,用前缀和 O(1) 计算,再按四边形不等式的决策单调性用分治把逐层转移从 O(PN²) 降到 O(PN log N)。
OJ: roj
题目 ID: 3178
难度:提高
标签:动态规划四边形不等式决策单调性分治优化前缀和python
创建: 2026-10-01 23:33
更新: 2026-10-10 13:00
形式化题目
一条直线上有
最小,输出这个最小值。坐标互不相同,
注意"建在其中的
正解
思路
第一步:排序,并说明可以只考虑"连续的一段"。
邮局只关心坐标之间的相对顺序,先把村庄按坐标升序排成
设某两个相邻村庄
把前缀
切成 段(每段非空、段内连续),使各段的"单邮局代价"之和最小。
第二步:写成 DP。
设
这里
第三步:
这一段只有一个邮局,建在中位数村庄上最优。把邮局从位置
样例里
第四步:找出真正的瓶颈。
朴素做法对每个
满足四边形不等式 。
这条不等式正是"交叉优于包含"的代数表达——两段交叉分配比一大一小包含分配更省。它的标准推论是决策单调性:
即"前 problem-analysis-workspace/viz_render.py 复现):
| 层 | ||||||||
|---|---|---|---|---|---|---|---|---|
| 0 | 2 | 3 | 5 | 9 | 21 | 37 | 43 | |
| 1 | 3 | 3 | 3 | 3 | 7 | 8 | 8 | |
| 0 | 1 | 2 | 3 | 5 | 9 | 21 | 27 | |
| 2 | 3 | 3 | 5 | 5 | 7 | 8 | 8 |
每一行的
第五步:用分治把每层降到
有了决策单调性,算一层 fill(lo, hi, opt_lo, opt_hi) 表示"
- 取中点
; - 只在
里扫一遍,求出 与 ; - 左半边
的分割点一定 ,右半边一定 ,各自带新界递归。
第 2 步里的 min(mid-1, opt_hi) 上界不能省:它同时保证
下面这张图画出样例中
flowchart LR
subgraph tree ["分治:算 ndp[2..10],opt 界 [1, 9]"]
mid["mid = 6<br/>扫 j∈[1,5] → ndp[6]=5, opt=3"]
mid --> L["左 [2,5],opt 界 [1, 3]"]
mid --> R["右 [7,10],opt 界 [3, 9]"]
L --> L2["mid = 3<br/>扫 j∈[1,2] → ndp[3]=1, opt=1"]
R --> R2["mid = 8<br/>扫 j∈[3,7] → ndp[8]=21, opt=7"]
end
关键在于每个 ndp[3] 时只需看 ndp[8] 时虽然窗口较大,但它已经把右半边的
最后还要处理一个边界:若
代码
segment_cost 是上面第三步的中位数公式(即 layer 负责一整层 fill 就是第五步的分治,靠 opt_lo / opt_hi 两个界把每层的扫描量压到 solve 只做读入、排序、前缀和、逐层推进和输出。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 13:00
* update_at: 2026-10-10 13:00
*/
// 邮局:DP + 分治优化(代价满足四边形不等式,最优分割点单调)
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll INF = 1000000000000000000LL;
int n, p;
vector<ll> xs; // 排好序的村庄坐标
vector<ll> pre; // pre[i] = 前 i 个村庄的坐标和
vector<ll> dp, ndp;
// 把村庄 l..r(下标闭区间)全交给一个邮局时的最小距离和,建在中位数上最优
ll segment_cost(int l, int r) {
if (l > r) {
return 0;
}
int mid = (l + r) / 2;
ll c = xs[mid];
ll left = c * (mid - l + 1) - (pre[mid + 1] - pre[l]);
ll right = (pre[r + 1] - pre[mid + 1]) - c * (r - mid);
return left + right;
}
// 算 ndp[lo..hi],已知这些位置的最优分割点都落在 [opt_lo, opt_hi]
void fill_range(int lo, int hi, int opt_lo, int opt_hi) {
if (lo > hi) {
return;
}
int mid = (lo + hi) / 2;
ll best = INF;
int best_j = opt_lo;
int lim = min(mid - 1, opt_hi);
for (int j = opt_lo; j <= lim; j++) {
ll v = dp[j] + segment_cost(j, mid - 1);
if (v < best) {
best = v;
best_j = j;
}
}
ndp[mid] = best;
fill_range(lo, mid - 1, opt_lo, best_j); // 左半边的最优分割点不超过 mid 的
fill_range(mid + 1, hi, best_j, opt_hi); // 右半边的最优分割点不小于 mid 的
}
int main() {
if (scanf("%d %d", &n, &p) != 2) {
return 0;
}
xs.assign(n, 0);
for (int i = 0; i < n; i++) {
scanf("%lld", &xs[i]);
}
sort(xs.begin(), xs.end()); // 邮局只关心相对顺序
pre.assign(n + 1, 0);
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + xs[i];
}
if (p >= n) { // 邮局够多,每个村庄都建一个
printf("0\n");
return 0;
}
dp.assign(n + 1, INF);
dp[0] = 0; // 0 个邮局:只有“前 0 个村庄”代价为 0
for (int k = 1; k <= p; k++) {
ndp.assign(n + 1, INF);
fill_range(k, n, k - 1, n - 1);
dp = ndp;
}
printf("%lld\n", dp[n]);
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 23:33
# update_at: 2026-10-01 23:33
import sys
from itertools import accumulate
INF = 10 ** 18 # “不可达”哨兵;真实答案不超过 N × 10000 = 3 × 10⁶,远小于它
def segment_cost(xs: list[int], pre: list[int], l: int, r: int) -> int:
"""把村庄 l..r(闭区间,按下标)全交给一个邮局时的最小距离和。
坐标已升序,建在中位数的那个村庄上最优;左右两侧各自用前缀和一次算完。
l > r 是空区间,返回 0。
"""
mid = (l + r) // 2
c = xs[mid] # 中位数村庄的坐标
left = c * (mid - l + 1) - (pre[mid + 1] - pre[l])
right = (pre[r + 1] - pre[mid + 1]) - c * (r - mid)
return left + right
def layer(dp: list[int], xs: list[int], pre: list[int], k: int) -> list[int]:
"""由“k-1 个邮局”的代价表 dp 推出“k 个邮局”的代价表 ndp[0..n]。
ndp[i] = min_{j<i} dp[j] + cost(j, i-1):前 j 个村庄交给 k-1 个邮局,
第 k 个邮局负责剩下的 j..i-1 这一段。代价函数满足四边形不等式,
所以最优分割点 j 随 i 单调不减,于是用分治把每层的 O(n²) 降成 O(n log n)。
"""
n = len(xs)
ndp = [INF] * (n + 1)
def fill(lo: int, hi: int, opt_lo: int, opt_hi: int) -> None:
"""算 ndp[lo..hi],已知这些位置的最优分割点都落在 [opt_lo, opt_hi]。"""
if lo > hi:
return
mid = (lo + hi) // 2
best, best_j = INF, opt_lo
for j in range(opt_lo, min(mid - 1, opt_hi) + 1):
value = dp[j] + segment_cost(xs, pre, j, mid - 1)
if value < best:
best, best_j = value, j
ndp[mid] = best
fill(lo, mid - 1, opt_lo, best_j) # 左半边的最优分割点不超过 mid 的
fill(mid + 1, hi, best_j, opt_hi) # 右半边的最优分割点不小于 mid 的
fill(k, n, k - 1, n - 1)
return ndp
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, p = next(data), next(data)
xs = sorted(next(data) for _ in range(n)) # 邮局只关心相对顺序,先排序
pre = [0, *accumulate(xs)] # pre[i] = 前 i 个村庄的坐标和
if p >= n: # 邮局够多,每个村庄都建一个
print(0)
return
dp = [INF] * (n + 1) # 0 个邮局:只有“前 0 个村庄”代价为 0
dp[0] = 0
for k in range(1, p + 1):
dp = layer(dp, xs, pre, k)
print(dp[n])
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:排序
;内层分治每层 ,共 层,合计 。朴素做法为 ; 时两者实测都在 s 量级,但分治版与题目标签"四边形不等式"给出的标准写法同阶。 - 空间复杂度:
dp、ndp、pre各,递归栈 ,合计 ;实测峰值约 MB,远低于 MB 限制。
数据说明
数据仓 10 个 .out 是生成事故文件:第 1 行形如 N P 答案,第 2 行把输入坐标原样回显。例如 POST3.out 开头是
10 5 9
1 2 3 6 7 9 11 22 44 50而题面的输出格式只要求"输出一个整数",ROJ 官网该题的样例输出也正是单独的一个 9;两个官方数据镜像(csps.de5.net、oidata.cc.cd)的 3178.tar.zst 里都是同样内容,说明这是上游数据缺陷而非本地损坏。逐字节比对对任何符合题面的解都必然失败。
本次按仓库既有先例(同 roj/3006)处理:把每个 .out 收敛为第 1 行的第 3 个字段(真实答案),并同步 data.json 的文件大小。修复前先用一份独立编写的 main.py、.out 内嵌答案三者完全一致(如 POST7 的 verify.sh 3178 为 10/10 PASS,单测例约
此外按任务要求未提供 brute.cpp,改为随机小数据自检:
图示解析
样例
flowchart TD
xs["升序坐标 1 2 3 6 7 9 11 22 44 50"] --> s1["段 A: 1,2,3 → 邮局在 2,代价 2"]
xs --> s2["段 B: 6,7,9,11 → 邮局在 7,代价 7"]
xs --> s3["段 C: 22 → 邮局在 22,代价 0"]
xs --> s4["段 D: 44 → 邮局在 44,代价 0"]
xs --> s5["段 E: 50 → 邮局在 50,代价 0"]
s1 --> total["总代价 2+7+0+0+0 = 9"]
s2 --> total
s3 --> total
s4 --> total
s5 --> total
读图要点:每一段内部的邮局都建在该段的中位数村庄上。段 A 的
总结
- 题目是选择问题:邮局只能建在给定村庄上,排序后每个邮局只需负责一段连续下标区间。
- 单段最优解建在中位数村庄,配合前缀和可以
求出任意区间的代价 。 - 朴素 DP 是
;利用 的四边形不等式得到决策单调性 ,再用分治收窄每格的分割点枚举范围,降到 。 - 实现上必须注意
的扫描上界取 :既保证段非空,又保证不越出已知范围。 时答案为 ,直接短路。