链上选 n 个村建小学:前缀和定位后,“去最近学校”等价于把链切成 n 个连续段,每段代价是到中位村的距离和,O(m^3) 区间划分类 DP 直接转移。
OJ: roj
题目 ID: 1197
难度:普及+/提高-
标签:动态规划区间DP中位数python
创建: 2026-09-29 23:03
更新: 2026-10-07 12:15
形式化题目
一条链上有
即所有村庄到最近小学的距离总和。样例:3 1 3 1 1 1 1 1 3,最优值为
正解
思路
一句话本质:把"选
第一步:坐标化。 用前缀和
第二步:「去最近学校」⟺「连续分段」。 直接枚举在哪些村建小学是
交换指派代价不增;反复交换直到无交错,
第三步:单段的最优选址是中位村。 固定段为村
用教学小样例看单段代价:3 1 3 1 1,坐标为 - 表示
| 1 | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|
| 1 | 0 | 3 | 4 | 8 | 12 | 17 |
| 2 | - | 0 | 1 | 4 | 8 | 10 |
| 3 | - | - | 0 | 3 | 4 | 6 |
| 4 | - | - | - | 0 | 1 | 2 |
| 5 | - | - | - | - | 0 | 1 |
| 6 | - | - | - | - | - | 0 |
观察重点:对角线全为 0(一个村的段建在本村);固定
第四步:区间划分类 DP。 设
按"最后一所学校负责的段"转移,枚举前一所学校的边界
| 0 | 3 | 4 | 8 | 12 | 17 | |
| - | 0 | 1 | 4 | 5 | 6 |
观察重点:-),代码里直接不枚举。
对样例 2(
村号 1 2 3 4 5 6 7 8 9 10
坐标 0 3 4 7 8 9 10 11 12 15
|-- 第 1 段 --][------ 第 2 段 ------]
▲ 学校(村2) ▲ 学校(村7)
代价 3 + 0 + 1 | 3 + 2 + 1 + 0 + 1 + 2 + 5 = 4 + 14 = 18逐个枚举其他切点
代码
dis[i] 是前缀坐标;cost 用 functools.cache 记忆化(DP 转移中同一段会被反复询问);dp[j][i] 的转移写成生成器 min。没有暴力层:模型等价性由上文双向论证保证,正确性由官方真实数据验证。
Python 版:
#!/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-09-29 23:03
# update_at: 2026-09-29 23:03
import sys
from functools import cache
INF = 10**15 # “尚未转移”的哨兵,取远大于所有距离和上界
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
m, n = next(data), next(data)
# dis[i]:第 i 个村到第 1 个村的距离(前缀和),村编号 1..m
dis = [0, 0] + [0] * (m - 1)
for i in range(2, m + 1):
dis[i] = dis[i - 1] + next(data)
@cache
def cost(l: int, r: int) -> int:
"""村 l..r 共用一所小学(建在中位村 mid)时,各村到学校的距离和。"""
mid = dis[(l + r) // 2] # 中位村的位置
return sum(abs(dis[v] - mid) for v in range(l, r + 1))
# dp[j][i]:前 i 个村建 j 所小学的最小距离和;转移枚举最后一所学校负责的村区间
dp: list[list[int]] = [[INF] * (m + 1) for _ in range(n + 1)]
for i in range(1, m + 1):
dp[1][i] = cost(1, i)
for j in range(2, n + 1):
for i in range(j, m + 1):
dp[j][i] = min(dp[j - 1][k] + cost(k + 1, i) for k in range(j - 1, i))
print(dp[n][m])
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-04 23:23
* update_at: 2026-10-04 23:23
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXM = 505;
const int MAXN = 505;
const ll INF = 1e18; // “尚未转移”的哨兵,远大于所有距离和的上界
ll m, n;
ll pos[MAXM]; // pos[i]:第 i 个村到第 1 个村的距离(前缀和)
ll cost[MAXM][MAXM]; // cost[l][r]:村 l..r 共用一所小学(建在中位村)时,段内的距离和
ll dp[MAXN][MAXM]; // dp[j][i]:前 i 个村恰好建 j 所小学的最小距离和
// 预处理每一段的代价:段内小学建在中位村 (l+r)/2 处,逐村累加距离。
void build_cost() {
for (ll l = 1; l <= m; l++) {
for (ll r = l; r <= m; r++) {
ll mid = (l + r) / 2; // 村庄编号与坐标同序,中位村就是编号中位
ll sum = 0;
for (ll v = l; v <= r; v++) {
sum += llabs(pos[v] - pos[mid]);
}
cost[l][r] = sum;
}
}
}
void solve() {
build_cost();
// 只建 1 所小学时,前 i 个村全归这一所学校负责。
for (ll i = 1; i <= m; i++) {
dp[1][i] = cost[1][i];
}
// 转移:枚举最后一所学校负责的段 [k+1, i];k 从 j-1 起保证前面每所至少管一个村。
for (ll j = 2; j <= n; j++) {
for (ll i = j; i <= m; i++) {
ll best = INF;
for (ll k = j - 1; k <= i - 1; k++) {
ll cur = dp[j - 1][k] + cost[k + 1][i];
if (cur < best) {
best = cur;
}
}
dp[j][i] = best;
}
}
cout << dp[n][m] << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
pos[1] = 0;
for (ll i = 2; i <= m; i++) {
ll d;
cin >> d;
pos[i] = pos[i - 1] + d;
}
solve();
return 0;
}复杂度
- 时间复杂度:
—— 个 值各花 计算,DP 转移 ;与 C++ 标准解同阶。 时约 量级的基本操作,Python 实测单点约 1 s(本地放宽限时下 10/10 通过;官方 1000 ms 限制下 Python 可能 TLE,属教学短解法允许范围)。 - 空间复杂度:
—— @cache的代价表与的 DP 表,实测峰值内存约 30 MB。
总结
本题是"选择型问题 → 分段型 DP"的经典转化:先用前缀和把链坐标化,再以交叉不等式证明"去最近学校"等价于"连续分段",把指数级的选址枚举压成
图示解析
这张图串起本题从建模到得到答案的主线:
链上 m 个村(相邻距离)
`- 前缀和 → 数轴上的有序点列
`- 交叉不等式: 去最近学校 ⟺ 切成 n 个连续段
`- 单段代价 = 到中位村((l+r)//2)的距离和
`- 区间划分 DP: dp[j][i] = min(dp[j-1][k] + cost(k+1,i))
`- 答案 dp[n][m]先看每个节点代表的推理结论(坐标化、分段等价、中位选址、DP 转移),再顺着分支检查它如何消除上一层的枚举维度:坐标化消掉"距离序列",分段等价消掉"组合选择",中位村消掉"段内选址",DP 消掉"切点组合"。图中只保留正文已经论证过的关键关系,不替代正文中的样例推演和正确性说明。