距离和的最小值
给定直线上 \(n\) 个点,其坐标分别为 \(x_1, x_2, \dots, x_n\)。需要在这条直线上找一个点 \(P(x)\),使得 \(P\) 到所有已知点的绝对距离之和最小,即最小化目标函数 \(f(x) = \sum \limits_{i=1}^n |x - x_i|\)。
结论
当 \(x\) 取这组数的大小的中位数时,\(f(x)\) 取得最小值。
证明
考虑任意两点 \(x_i\) 和 \(x_j\)(设 \(x_i \lt x_j\)),根据几何意义,要使 \(|x-x_i| + |x-x_j|\) 最小,\(x\) 必须落在 \([x_i,x_j]\) 区间内,此时两距离之和恒等于 \(x_j - x_i\)。因此,将 \(n\) 个点从小到大排序后,首尾两两配对:
- \((x_1, x_n)\) 要求 \(x\) 在它们之间;
- \((x_2, x_{n-1})\) 要求 \(x\) 在它们之间……
- 不断向内收缩,最终所有区间的交集正是这组数据的中位数位置。
若 \(n\) 为偶数,中位数是一个区间 \([x_{n/2}, x_{n/2+1}]\),区间内任意一点均可使距离和达到最小。
例题:P10452 货仓选址
参考代码 $O(n \log n)$
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int a[N];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
// 解决货仓选址问题的关键是找到所有坐标的中位数,并将所有点到中位数的距离相加。
// 可以使用一个更巧妙的数学技巧来直接计算最小距离和,而无需显式找出中位数。
// 1. 首先对所有坐标进行排序。
sort(a + 1, a + n + 1);
ll ans = 0;
// 2. 最小距离和等于排序后对称位置的坐标差之和。
// 这个结论的含义是 sum(|a[i] - median|) = (a[n] - a[1]) + (a[n-1] - a[2]) + ...
// 循环通过配对最远和最近、次远和次近的点,来直接累加这个总和。
for (int i = 1; i * 2 <= n; i++) {
ans += a[n - i + 1] - a[i];
}
printf("%lld\n", ans);
return 0;
}
参考代码 $O(n)$
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = (int)1e5 + 5;
int a[N];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
nth_element(a, a + n / 2, a + n);
int m = a[n / 2], ans = 0;
for (int i = 0; i < n; i++) {
ans += abs(a[i] - m);
}
cout << ans << "\n";
return 0;
}
在二维平面上有 \(n\) 个点 \((x_i,y_i)\),要求找一个点 \((X,Y)\),使得该点到所有点的曼哈顿距离之和最小,即 \(\min \sum \limits_{i=1}^n (|X-x_i| + |Y-y_i|)\)。
结论
由于曼哈顿距离的 \(x\) 轴坐标与 \(y\) 轴坐标互不干扰,目标函数可以拆分为 \(\sum |X-x_i| + \sum |Y-y_i|\)。分别求出 \(x\) 序列的中位数 \(X\) 和 \(y\) 序列的中位数 \(Y\),组合成 \((X,Y)\) 即为所求。
例题:P1889 士兵站队
给定 \(n\) 个士兵在网格点上的坐标 \((x_i,y_i)\),他们可以沿网格上、下、左、右移动。目标是将他们排成一个水平队列,坐标分别为 \((x,y),(x+1,y),\dots,(x+n-1,y)\),求出最少的总移动步数。
解题思路
总移动步数是每个士兵移动步数之和,由于士兵只能沿网格移动,移动步数即为曼哈顿距离 \(\sum \limits_{i=1}^n |x'_i-x_i| + \sum\limits_{i=1}^n |y'_i - y_i|\)。由于 \(x\) 方向和 \(y\) 方向的移动是独立的,可以分别求解两个维度的最小值。
最终所有士兵的纵坐标都必须是同一个值 \(y'\),目标是最小化 \(\sum\limits_{i=1}^n |y_i - y'|\)。这是一个经典的中位数问题,根据绝对值不等式,当 \(y'\) 取 \(y_1,y_2, \dots, y_n\) 的中位数时,距离之和最小。
最终士兵的横坐标应为 \(x_s,x_s+1,\dots,x_s+n-1\),为了使总移动步数最小,应该让初始横坐标较小的士兵排在目标队列中较靠左的位置(即排序后的第 \(i\) 个士兵对应目标位置的第 \(i\) 个坐标)。设排序后的横坐标为 \(x'_0, x'_1, \dots, x'_{n-1}\),目标是最小化 \(\sum\limits_{i=1}^n |x'_i - (x_s+i)|\),将公式变形,得到 \(\sum\limits_{i=0}^{n-1} |(x'_i-i)-x_s|\)。令 \(z_i=x'_i-i\),则问题转化为最小化 \(\sum\limits_{i=0}^{n-1} |z_i-x_s|\),这样就再次变成了一个中位数问题。
这里对 \(x\) 的排序是必须的,因为在代入 \(X'_i = X_i - i\) 这个公式前,等式右边的 \(i\) 必须严格对应升序排列后的第 \(i\) 小的士兵,因此时间复杂度为 \(O(n\log n)\)。
参考代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = (int)1e4 + 5;
int x[N], y[N];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> x[i] >> y[i];
}
sort(x, x + n);
for (int i = 0; i < n; i++) {
x[i] -= i;
}
nth_element(x, x + n / 2, x + n);
nth_element(y, y + n / 2, y + n);
int mx = x[n / 2], my = y[n / 2], ans = 0;
for (int i = 0; i < n; i++) {
ans += abs(x[i] - mx) + abs(y[i] - my);
}
cout << ans << "\n";
return 0;
}
习题:CF1486B Eastern Exhibition
在二维平面上有 \(n \ (1 \le n \le 1000)\) 座房子,第 \(i\) 座房子的整数坐标为 \((x_i,y_i) \ (0 \le x_i, y_i \le 10^9)\)。现在需要选择一个整数坐标点建设“东方展览会”,使得所有房子到展览会的曼哈顿距离之和最小。要求计算有多少个不同的整数坐标点,能够同时满足该距离和的要求。
解题思路
当 \(n\) 为奇数时,全局最优解有且仅有一个,即为正中间的中位数,直接输出 \(1\) 即可。
当 \(n\) 为偶数时,全局最优解的坐标范围是两个区间围成的矩形区域,需要找出 \(x\) 序列和 \(y\) 序列各自的两个中位数,代表合法区间 \([x_l, x_r]\) 和 \([y_l, y_r]\),则可选的整数点的数量为 \((x_r-x_l+1) \times (y_r-y_l+1)\)。
参考代码
#include <iostream>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1005;
int x[N], y[N];
void solve() {
int n; cin >> n;
for (int i = 0; i < n; i++) {
cin >> x[i] >> y[i];
}
if (n % 2 == 1) {
cout << "1\n";
} else {
int mid = n / 2;
nth_element(x, x + mid, x + n);
int xr = x[mid];
nth_element(x, x + mid - 1, x + mid);
int xl = x[mid - 1];
nth_element(y, y + mid, y + n);
int yr = y[mid];
nth_element(y, y + mid - 1, y + mid);
int yl = y[mid - 1];
cout << (ll)(xr - xl + 1) * (yr - yl + 1) << "\n";
}
}
int main()
{
int t; cin >> t;
while (t--) {
solve();
}
return 0;
}
如果数轴上的每个点都有一个正权值 \(w_i\),这就是带权距离和问题,要最小化带权绝对距离之和 \(f(x) = \sum\limits_{i=1}^n w_i \cdot |x_i-x|\)。
结论
对于一组已经按坐标从小到大排序的点 \((x_1, w_1), (x_2, w_2), \dots, (x_n, w_n)\),令总权重为 \(W = \sum_{i=1}^n w_i\)。从左往右累加权重,当累加的权重第一次达到或超过总和的一半时,该位置对应的坐标即为带权中位数。
证明
扰动法:假设当前选址在 \(x_k\),若将其向右移动一个微小的距离 \(\Delta\),则 \(x_k\) 左侧的所有点到新选址的距离都会增加 \(\Delta\),总代价增加 \(\Delta \cdot \sum_{i=1}^k w_i\);而 \(x_k\) 右侧的所有点到新选址的距离都会减少 \(\Delta\),总代价减少 \(\Delta \cdot \sum_{i=k+1}^n w_i\)。要让向右移动不优(总代价不减少),必须满足 \(\sum \limits_{i=1}^k w_i \ge \sum \limits_{i=k+1}^n w_i\)。两边同时加上左边,得 \(2 \cdot \sum_{i=1}^k w_i \ge W\),即 \(\sum_{i=1}^k w_i \ge \frac{W}{2}\)。同理可证向左移动不优的条件,因此,总权重越过半数的那个转折点就是全局最优解。
例题:P3819 松江 1843 路
参考代码 $O(n \log n)$
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
using ll = long long;
const int N = 100005;
struct House {
ll x;
int r;
};
House a[N];
int main()
{
ll l; int n;
scanf("%lld%d", &l, &n);
int sumr = 0;
for (int i = 1; i <= n; i++) {
scanf("%lld%d", &a[i].x, &a[i].r);
sumr += a[i].r;
}
sort(a + 1, a + n + 1, [](House &h1, House &h2) {
return h1.x < h2.x;
});
int pos = -1;
ll sum = 0;
for (int i = 1; i <= n; i++) {
sum += a[i].r;
if (pos == -1 && sum * 2 >= sumr) {
pos = i;
}
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans += abs(a[i].x - a[pos].x) * a[i].r;
}
printf("%lld\n", ans);
return 0;
}
参考代码 $O(n)$
如果采用类似快速选择的算法找加权中位数,平均时间复杂度能做到 \(O(n)\)。
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
using namespace std;
using ll = long long;
const int N = (int)1e5 + 5;
struct House {
ll x;
int r;
};
House a[N];
int sum(int l, int r) {
int res = 0;
for (int i = l; i <= r; i++) {
res += a[i].r;
}
return res;
}
int main()
{
ll l; int n;
cin >> l >> n;
int w = 0;
for (int i = 0; i < n; i++) {
cin >> a[i].x >> a[i].r;
w += a[i].r;
}
ll m = -1, pre = 0, suf = 0;
int left = 0, right = n - 1;
while (left <= right) {
if (left == right) {
m = a[left].x;
break;
}
int p = left + rand() % (right - left + 1);
ll px = a[p].x;
int i = left, j = right, k = left;
// 三向切分
while (k <= j) {
if (a[k].x < px) {
swap(a[i], a[k]);
i++; k++;
} else if (a[k].x > px) {
swap(a[k], a[j]);
j--;
} else {
k++;
}
}
int ls = sum(left, i - 1), eq = sum(i, j), gt = sum(j + 1, right);
if (pre + ls > w / 2) {
right = i - 1;
suf += eq + gt;
} else if (suf + gt > w / 2) {
left = j + 1;
pre += eq + ls;
} else {
m = px;
break;
}
}
// 计算总距离
ll ans = 0;
for (int i = 0; i < n; i++) {
ans += abs(a[i].x - m) * a[i].r;
}
cout << ans << "\n";
return 0;
}
例题:P2512 [HAOI2008] 糖果传递
有 \(n \ (1 \le n \le 10^6)\) 个小朋友围成一圈,第 \(i\) 个小朋友初始有 \(a_i \ (1 \le a_i \le 1.5 \times 10^9)\) 个糖果,保证 \(\sum_{i=1}^n a_i\) 是 \(n\) 的倍数。每个人只能给左右相邻的人传递糖果,每传递 1 个糖果需要消耗 1 个单位的代价,求使所有人手中的糖果数量均等时的最小总代价。
解题思路
设每个人最终持有的平均糖果数为 \(\text{avg} = \frac{\sum a_i}{n}\)。
假设第 \(i\) 个小朋友给第 \(i-1\) 个小朋友传递了 \(x_i\) 个糖果(若 \(x_i \lt 0\),表示第 \(i-1\) 个给第 \(i\) 个传递了 \(|x_i|\) 个糖果),特别地,\(x_1\) 表示第 \(1\) 个小朋友给第 \(n\) 个小朋友传递的糖果数。
对于每个人,其最终的糖果数量应等于 \(\text{avg}\):
- 第 \(1\) 个小朋友:\(a_1 - x_1 + x_2 = \text{avg} \implies x_2 = x_1 - (a_1 - \text{avg})\)。
- 第 \(2\) 个小朋友:\(a_2 - x_2 + x_3 = \text{avg} \implies x_3 = x_2 - (a_2 - \text{avg}) = x_1 - ((a_1 - \text{avg}) + (a_2 - \text{avg}))\)。
- 以此类推,对于第 \(i\) 个小朋友,有 \(x_i = x_1 - \sum\limits_{k=1}^{i-1} (a_k - \text{avg})\)。
定义前缀和序列 \(S\):令 \(c_i = a_i - \text{avg}\),设 \(S_i = \sum\limits_{k=1}^i c_i\)(规定 \(S_0 = 0\)),则有 \(x_i = x_1 - S_{i-1}\)。
题目要求最小化传递的总代价,即最小化所有 \(|x_i|\) 之和 \(\sum\limits_{i=1}^n |S_{i-1} - x_1|\)。
这转化为经典的绝对值之和最小化问题:在数轴上有 \(n\) 个点 \(S_0, S_1, S_2, \dots, S_{n-1}\),需要选择一个点 \(x_1\),使得这 \(n\) 个点到 \(x_1\) 的绝对距离之和最小。
根据绝对值三角不等式的性质,当 \(x_1\) 取点集 \(S\) 的中位数时,绝对距离之和取得最小值。
参考代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
using ll = long long;
const int N = (int)1e6 + 5;
int a[N], s[N];
int main()
{
int n; cin >> n;
ll sum = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
sum += a[i];
}
int avg = sum / n;
s[0] = a[0] - avg;
for (int i = 1; i < n; i++) {
s[i] = s[i - 1] + a[i] - avg;
}
int mid = n / 2;
nth_element(s, s + mid, s + n);
ll m = s[mid], ans = 0;
for (int i = 0; i < n; i++) {
ans += abs(s[i] - m);
}
cout << ans << "\n";
return 0;
}
习题:P10453 七夕祭
在一个 \(N \times M \ (1 \le N,M \le 100000)\) 的矩形网格会场中,分布着 \(T \ (0 \le T \le \min(N \times M, 100000))\) 个感兴趣的摊点,分别用坐标 \((x,y) \ (1 \le x \le N, 1 \le y \le N)\) 给出。可以交换相邻两个位置的摊点(每一行/列的首尾两个位置也算作相邻,即网格是环形包绕的)。希望通过最少的交换次数:
- 使每一行包含感兴趣的摊点数量相同。
- 使每一列包含感兴趣的摊点数量相同。
求最多能满足上述哪几个要求(
both、row、column或impossible),并输出最少的交换次数。
解题思路
同 P2512 [HAOI2008] 糖果传递 的分析过程,行、列之间可以独立考虑。
参考代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
using ll = long long;
const int N = 100005;
int row[N], col[N], s[N];
// 求解一维环形均分问题的函数
ll solve(int n, int a[]) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += a[i];
}
// 无法整除,说明无法平分
if (sum % n != 0) return -1;
int avg = sum / n;
// 计算差值前缀和数组
s[0] = a[0] - avg;
for (int i = 1; i < n; i++) {
s[i] = s[i - 1] + a[i] - avg;
}
// 寻找中位数
nth_element(s, s + n / 2, s + n);
int m = s[n / 2];
// 计算到中位数的距离总和
ll ans = 0;
for (int i = 0; i < n; i++) {
ans += abs(s[i] - m);
}
return ans;
}
int main()
{
int n, m, t; cin >> n >> m >> t;
while (t--) {
int r, c; cin >> r >> c;
row[r - 1]++; col[c - 1]++;
}
ll ansr = solve(n, row);
ll ansc = solve(m, col);
// 逻辑分类判断输出
if (ansr != -1 && ansc != -1) {
cout << "both " << ansr + ansc << "\n";
} else if (ansr != -1) {
cout << "row " << ansr << "\n";
} else if (ansc != -1) {
cout << "column " << ansc << "\n";
} else {
cout << "impossible\n";
}
return 0;
}

浙公网安备 33010602011771号