惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园_首页
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
N
News and Events Feed by Topic
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
IT之家
IT之家
W
WeLiveSecurity
P
Proofpoint News Feed
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
月光博客
月光博客
Schneier on Security
Schneier on Security
博客园 - 三生石上(FineUI控件)
Application and Cybersecurity Blog
Application and Cybersecurity Blog
腾讯CDC
H
Heimdal Security Blog
Y
Y Combinator Blog
Engineering at Meta
Engineering at Meta
量子位
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
V
Visual Studio Blog
L
LangChain Blog
Last Week in AI
Last Week in AI
The Cloudflare Blog
Hacker News: Ask HN
Hacker News: Ask HN
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
A
Arctic Wolf
T
Tenable Blog
I
Intezer
P
Privacy International News Feed
Attack and Defense Labs
Attack and Defense Labs
N
News | PayPal Newsroom
Martin Fowler
Martin Fowler

博客园 - RonChen

区间合并 归并排序与逆序对 快速排序与快速选择 同余分析 差分约束 Treap 点分治 莫队算法 分块 扫描线 错排问题 Sprague-Grundy (SG) 函数及其应用 容斥原理 卢卡斯定理 线性基 高斯消元 勒让德公式 次短路 分层图最短路 01 图最短路 洪水填充 双向搜索 迭代加深搜索 剪枝 最小表示法 表达式计算 KMP 算法 队列 高精度运算
距离和的最小值
RonChen · 2026-07-19 · via 博客园 - RonChen

给定直线上 \(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;
}

如果数轴上的每个点都有一个正权值 \(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;
}