把单个站点总代价看成绝对值和函数,先取中位数区间平台,再从左右两条单调代价序列中归并取前 k 小。
OJ: luogu
题目 ID: P4998
难度:普及+/提高
标签:数学贪心中位数思维
日期: 2026-06-20 13:25
题意
数轴上有 n 户人家,位置为 a_i。
现在要建 k 个信号站,每个站必须放在不同的整数位置上。
如果某个信号站建在位置 x,它的不合理值是:
sum |x - a_i|
题目要求这 k 个信号站不合理值之和最小。
思路
先看一个可以直接验证想法的朴素解:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXN = 105;
int n, k;
int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
int min_a = (int)1e9;
int max_a = -(int)1e9;
for (int i = 1; i <= n; i++) {
cin >> a[i];
min_a = min(min_a, a[i]);
max_a = max(max_a, a[i]);
}
vector<i64> cost_list;
// 小数据暴力:把候选位置限制在 [min_a-k, max_a+k],
// 直接算每个整数位置的总代价,再取最小的 k 个。
for (int x = min_a - k; x <= max_a + k; x++) {
i64 cost = 0;
for (int i = 1; i <= n; i++) {
cost += llabs((i64)x - a[i]);
}
cost_list.push_back(cost);
}
sort(cost_list.begin(), cost_list.end());
i64 answer = 0;
for (int i = 0; i < k; i++) {
answer += cost_list[i];
}
cout << answer << '\n';
return 0;
}本题最重要的观察是:多个信号站之间并不会分摊住户。
所以如果定义:
f(x) = sum |x - a_i|
那么题目其实就是:
- 从所有整数位置里选
k个不同的x - 使
f(x)的和最小
接下来只要研究这个一维函数 f(x) 的形状。
对于绝对值和函数,最小值一定出现在中位数区间:
[a_{(n+1)/2}, a_{(n+2)/2}]
并且这个区间内的所有整数位置,函数值都相同。
这说明最小的候选位置先是一整段“平坦平台”。
再看平台两边:
- 往左越远,代价单调不降;
- 往右越远,代价也单调不降。
于是最小的 k 个函数值一定由三部分组成:
- 平台内部的所有位置
- 平台左边若干个最近位置
- 平台右边若干个最近位置
如果平台长度已经不少于 k,
那答案就是:
k * min_cost
否则先把平台全取上, 然后从左右两边的两条单调代价序列里归并取出最小的剩余若干项即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXN = 1000000 + 5;
const int MAXA = 1000000;
int n, k;
int a[MAXN];
int freq[MAXA + 1];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
freq[a[i]]++;
}
sort(a + 1, a + n + 1);
int left_mid = a[(n + 1) / 2];
int right_mid = a[(n + 2) / 2];
// 中位数区间内的所有整数位置,总代价都相同。
i64 min_cost = 0;
for (int i = 1; i <= n; i++) {
min_cost += llabs((i64)a[i] - left_mid);
}
i64 interval_len = (i64)right_mid - left_mid + 1;
if (k <= interval_len) {
cout << min_cost * k << '\n';
return 0;
}
i64 answer = min_cost * interval_len;
int need = k - (int)interval_len;
// 左侧代价序列:f(left_mid - 1), f(left_mid - 2), ...
// 右侧代价序列:f(right_mid + 1), f(right_mid + 2), ...
// 这两条序列都是单调不降的,直接归并取前 need 小即可。
int x_left = left_mid;
int cnt_less = lower_bound(a + 1, a + n + 1, left_mid) - (a + 1); // 严格小于 left_mid 的个数
i64 cost_left = min_cost;
int x_right = right_mid;
int cnt_le = upper_bound(a + 1, a + n + 1, right_mid) - (a + 1); // 小于等于 right_mid 的个数
i64 cost_right = min_cost;
while (need--) {
// 向左走一步:f(x-1) - f(x) = n - 2 * count(a_i < x)
i64 next_left = cost_left + (i64)n - 2LL * cnt_less;
// 向右走一步:f(x+1) - f(x) = 2 * count(a_i <= x) - n
i64 next_right = cost_right + 2LL * cnt_le - n;
if (next_left <= next_right) {
answer += next_left;
cost_left = next_left;
x_left--;
if (x_left >= 0) {
cnt_less -= freq[x_left];
}
} else {
answer += next_right;
cost_right = next_right;
x_right++;
if (x_right <= MAXA) {
cnt_le += freq[x_right];
}
}
}
cout << answer << '\n';
return 0;
}复杂度
排序
总结
这题关键不在“放多个站”,而在先把问题改写成:
- 取函数
f(x) = sum |x-a_i|的前k小整数点值。
一旦看到 f(x) 是中位数平台加左右单调上升的形状,
后面的做法就很自然了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
