距离和的最小值

给定直线上 \(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)\) 给出。可以交换相邻两个位置的摊点(每一行/列的首尾两个位置也算作相邻,即网格是环形包绕的)。希望通过最少的交换次数:

  1. 使每一行包含感兴趣的摊点数量相同。
  2. 使每一列包含感兴趣的摊点数量相同。

求最多能满足上述哪几个要求(bothrowcolumnimpossible),并输出最少的交换次数。

解题思路

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;
}
posted @ 2026-07-19 19:45  RonChen  阅读(4)  评论(0)    收藏  举报