「Soldiers」 士兵

GitHub跳转原题关系图返回列表

把二维代价按 x/y 两轴解耦:y 取中位数;x 先排序让第 i 小的人领第 i 个格子,再对 x[i]-i 取中位数,两轴代价相加即答案。

OJ: roj

题目 ID: 3027

难度:普及+/提高-

标签:排序中位数贪心

创建: 2026-10-01 10:51

更新: 2026-10-07 13:50

形式化题目

给定平面上 nn 个整点 (x1,y1),…,(xn,yn)(x_1, y_1), \dots, (x_n, y_n),n⩾1n \geqslant 1。

一次移动可以把某个点的横坐标或纵坐标加一/减一,代价为 11。求最小总代价,使得移动后这 nn 个点恰好占据

{(X,Y), (X+1,Y), …, (X+n−1,Y)}\{(X, Y),\ (X+1, Y),\ \dots,\ (X+n-1, Y)\}

这 nn 个互不相同的格子,其中 X,YX, Y 可取任意整数。

形式化地,设 σ\sigma 是士兵到格子编号的双射,要求

min⁡X, Y, σ ∑i=1n(∣xi−(X+σ(i))∣+∣yi−Y∣)\min_{X,\ Y,\ \sigma}\ \sum_{i=1}^{n} \Big( \bigl|x_i - (X + \sigma(i))\bigr| + |y_i - Y| \Big)

正解

思路

朴素想法:直接枚举三个决策量——目标行 YY、目标段起点 XX、以及士兵到格子的一一配对 σ\sigma。 坐标范围 ∣xi∣,∣yi∣⩽10000|x_i|, |y_i| \leqslant 10000 把 X,YX, Y 的候选压缩到 [−10000−n, 10000+n][-10000-n,\ 10000+n] 量级,但固定 (X,Y)(X, Y) 后还有 n!n! 种配对, 总工作量是天文数字。瓶颈是决策变量太多,所以必须把 σ\sigma、XX、YY 逐个用解析结论消掉,而不是枚举。

关键观察一:两条坐标轴可以分开算。 看代价的组成:yiy_i 只和 YY 一起出现,xix_i 只和自己被分到的格子编号一起出现, 两个方向之间没有交叉项;约束"yy 坐标相同"只约束 YY,"xx 坐标相邻"只约束 XX 与 σ\sigma。于是

answer=min⁡X,σ∑i∣xi−(X+σ(i))∣⏟fx  +  min⁡Y∑i∣yi−Y∣⏟fy\text{answer} = \underbrace{\min_{X,\sigma}\sum_i \bigl|x_i - (X+\sigma(i))\bigr|}_{f_x} \;+\; \underbrace{\min_{Y}\sum_i |y_i - Y|}_{f_y}

关键观察二:yy 方向取中位数。 g(Y)=∑i∣yi−Y∣g(Y) = \sum_i |y_i - Y| 是凸的分段线性函数:YY 每越过一个 yiy_i, 斜率就增加 22。把 yy 升序排成 y(0)≤⋯≤y(n−1)y_{(0)} \le \dots \le y_{(n-1)},则在区间 [y(k),y(k+1)][y_{(k)}, y_{(k+1)}] 上斜率为 2(k+1)−n2(k+1) - n,它在 k+1⩾n/2k+1 \geqslant n/2 时由负转正,最小值就在中位数处取得:

fy=∑i∣yi−y(⌊n/2⌋)∣f_y = \sum_i \bigl| y_i - y_{(\lfloor n/2 \rfloor)} \bigr|

关键观察三:固定 XX 后,最优配对是"排序后顺序发放"。 把 xx 升序排成 x(0)≤⋯≤x(n−1)x_{(0)} \le \dots \le x_{(n-1)}, 此时目标横坐标恰好是 X,X+1,…,X+n−1X, X+1, \dots, X+n-1。若出现"xx 较小的士兵被分到较靠右的格子、 xx 较大的士兵被分到较靠左的格子"这样的逆序,那么把这两人的目标格子交换一下, 一维绝对值的重排不等式保证总代价不增。 这条不等式就是成本函数 c(x,t)=∣x−t∣c(x,t)=|x-t| 的 Monge 性质:a⩽ba \leqslant b、P⩽QP \leqslant Q 时 ∣a−P∣+∣b−Q∣⩽∣a−Q∣+∣b−P∣|a-P|+|b-Q| \leqslant |a-Q|+|b-P|,按 a,ba, b 与 P,QP, Q 的四种位置关系分类拆开绝对值即可验证:

∣x(i)−(X+p)∣+∣x(j)−(X+q)∣  ⩽  ∣x(i)−(X+q)∣+∣x(j)−(X+p)∣(i<j, p<q)\bigl|x_{(i)} - (X+p)\bigr| + \bigl|x_{(j)} - (X+q)\bigr| \;\leqslant\; \bigl|x_{(i)} - (X+q)\bigr| + \bigl|x_{(j)} - (X+p)\bigr| \qquad (i<j,\ p<q)

反复消掉逆序,就得到"第 ii 小的人领第 ii 个格子"的顺序配对,配对方案 σ\sigma 就此被消掉。

关键观察四:xx 方向也化归成中位数。 把顺序配对代回去:

fx=min⁡X∑i=0n−1∣x(i)−(X+i)∣=min⁡X∑i=0n−1∣(x(i)−i)⏟zi−X∣f_x = \min_X \sum_{i=0}^{n-1} \bigl| x_{(i)} - (X+i) \bigr| = \min_X \sum_{i=0}^{n-1} \bigl| \underbrace{(x_{(i)} - i)}_{z_i} - X \bigr|

这和 yy 方向是同一个问题——“到一点的距离之和最小”,所以 XX 也取 zi=x(i)−iz_i = x_{(i)} - i 的中位数:

fx=∑i∣zi−z(⌊n/2⌋)∣f_x = \sum_i \bigl| z_i - z_{(\lfloor n/2 \rfloor)} \bigr|

直观上,ziz_i 是"假设整段的左端点放在 00,第 ii 个人还需要往左/往右平移多少", 取它的中位数就是把整段平移到左右两侧代价平衡的位置。

样例推演:n=5n = 5,坐标为 (1,2),(2,2),(1,3),(3,−2),(3,3)(1,2), (2,2), (1,3), (3,-2), (3,3)。

先看 yy 方向:y=[2,2,3,−2,3]y = [2,2,3,-2,3] 排序得 [−2,2,2,3,3][-2,2,2,3,3],下标 5/2=25/2 = 2 的元素是中位数 Y=2Y = 2。

士兵 yiy_i −2-2 22 22 33 33
∣yi−Y∣\lvert y_i - Y \rvert(Y=2Y=2) 44 00 00 11 11

得到 fy=4+0+0+1+1=6f_y = 4+0+0+1+1 = 6。

再看 xx 方向:x=[1,2,1,3,3]x = [1,2,1,3,3] 排序得 [1,1,2,3,3][1,1,2,3,3],减去下标得 z=[1−0, 1−1, 2−2, 3−3, 3−4]=[1,0,0,0,−1]z = [1-0,\ 1-1,\ 2-2,\ 3-3,\ 3-4] = [1,0,0,0,-1],排序后 [−1,0,0,0,1][-1,0,0,0,1], 中位数 X=z(2)=0X = z_{(2)} = 0,fx=1+0+0+0+1=2f_x = 1+0+0+0+1 = 2。

把两轴合起来看,最终的目标行是 Y=2Y = 2,目标横坐标是 X+i=0,1,2,3,4X+i = 0,1,2,3,4,配对如下:

士兵(按 xx 升序) (1,2)(1,2) (1,3)(1,3) (2,2)(2,2) (3,−2)(3,-2) (3,3)(3,3)
目标格子 (0,2)(0,2) (1,2)(1,2) (2,2)(2,2) (3,2)(3,2) (4,2)(4,2)
xx 位移 11 00 00 00 11
yy 位移 00 11 00 44 11
小计 11 11 00 44 22

总计 1+1+0+4+2=81+1+0+4+2 = 8,恰好是 fx+fy=2+6f_x + f_y = 2 + 6,与样例输出一致。

算法流程:对 xx 排序、错位成 x(i)−ix_{(i)} - i 再排序取中位数求和;对 yy 排序取中位数求和;两者相加输出。

代码

Python 版:

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-10-01 10:52
# update_at: 2026-10-04 13:03

import sys


def median_cost(values: list[int]) -> int:
    """把这些数平移到同一个点,最小的总距离 = 各点到中位数的绝对距离之和。"""
    values.sort()
    median = values[len(values) // 2]
    return sum(abs(v - median) for v in values)


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    pairs = [(next(data), next(data)) for _ in range(n)]  # 第 i 行的 (x[i], y[i])
    xs = [x for x, _ in pairs]
    ys = [y for _, y in pairs]

    # x 轴:目标是 n 个相邻格子 base, base+1, ..., base+n-1,排序后第 i 名士兵
    # 领第 i 个格子,因此把每人要走的偏移 i 提前扣掉,再对 base 取中位数。
    x_cost = median_cost([x - i for i, x in enumerate(sorted(xs))])
    # y 轴:全部站到同一行 Y,取 y 的中位数即可。
    print(x_cost + median_cost(ys))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * 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-06 14:54
 * update_at: 2026-10-06 14:54
 */

#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;

const int MAXN = 10005;

ll xs[MAXN]; // 士兵的 x 坐标,排序后按第 i 小的人领第 i 个格子错位
ll ys[MAXN]; // 士兵的 y 坐标

// 把这些数平移到同一个点,最小的总距离 = 各点到中位数的绝对距离之和。
// 数组按 a[1..n] 存储,调用时会被就地排序。
ll median_cost(ll a[], int n) {
    sort(a + 1, a + n + 1);
    ll median = a[n / 2 + 1]; // 下标从 1 开始的中位数位置
    ll total = 0;
    for (int i = 1; i <= n; i++) {
        ll diff = a[i] - median;
        if (diff < 0) {
            diff = -diff;
        }
        total += diff;
    }
    return total;
}

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%lld %lld", &xs[i], &ys[i]);
    }

    // x 轴:目标是 n 个相邻格子 X, X+1, ..., X+n-1。
    // 排序后第 i 小的人领第 i 个格子,于是把偏移 i-1 提前扣掉,再对 X 取中位数。
    sort(xs + 1, xs + n + 1);
    for (int i = 1; i <= n; i++) {
        xs[i] -= (i - 1);
    }
    ll x_cost = median_cost(xs, n);

    // y 轴:全部站到同一行 Y,取 y 的中位数即可。
    ll y_cost = median_cost(ys, n);

    printf("%lld\n", x_cost + y_cost);
    return 0;
}

median_cost 只回答一个问题——“把这些数平移到同一点,最小总距离是多少”, 所以 xx、yy 两个方向共用它;x 轴的错位 x - i 必须在 sorted(xs) 的结果上做, 因为只有排序之后"第 ii 小的人领第 ii 个格子"才成立。solve 只负责读入、两次调用和输出。

复杂度

  • 时间:两次排序各 O(nlog⁡n)O(n \log n),两次线性求和 O(n)O(n),总计 O(nlog⁡n)O(n \log n)。 n⩽10000n \leqslant 10000 时实测单组约 0.020.02 s,远低于 10001000 ms 时限。
  • 空间:两个长度 nn 的列表与两个 O(n)O(n) 的切片,总计 O(n)O(n);实测峰值内存约 1616 MB,低于 128128 MB。

总结

本题的解法可概括为三次"消元",每一次都把一类决策变量从枚举里彻底删掉:

  1. 轴的解耦消掉二维耦合——xx 与 yy 的代价可加,分别求最优即可;
  2. 重排不等式消掉配对方案 σ\sigma——排序后第 ii 小的人配第 ii 个格子最优;
  3. 中位数性质消掉连续量 X,YX, Y——"到一点的距离之和最小"的解就是取中位数。

剩下要做的只有两遍排序。把"多决策量联合枚举"简化成"若干独立的凸函数取中位数", 是绝对值最优化问题的通用套路:凡是决策变量进入代价时只通过 ∣v−未知量∣|v - \text{未知量}| 的形式, 就应该先想能不能把它化成中位数。

图示解析

下图给出从题面到答案的完整主线:左侧是三个待定决策量,中间是消元后的两轴一维问题, 右侧是最终的两遍排序实现。

text
输入 n 个点 (x_i, y_i)
   │  代价 = Σ|x_i - (X+σ(i))| + Σ|y_i - Y|     (x、y 之间没有交叉项)
   ▼
轴解耦:answer = f_x + f_y
   │
   ├── y 轴:固定 Y,代价 Σ|y_i - Y| 是凸函数,每越过一个点斜率 +2
   │          └─▶ Y = median(y),fy = Σ|y_i - median(y)|
   │
   └── x 轴:固定 X,先用重排不等式消掉配对 σ
              ├─▶ σ:排序后第 i 小的人领第 i 个格子
              ├─▶ 代回得 Σ|(x_(i) - i) - X|,又变成"到一点距离和最小"
              └─▶ X = median(x_(i) - i),fx = Σ|z_i - median(z)|
   │
   ▼
answer = fx + fy,两遍排序即可,复杂度 O(n log n)

这张图的关键在于中间那条分叉:题面里的三个决策量 σ,X,Y\sigma, X, Y 被分派到了两条互不干扰的轴上, yy 轴一步到位取中位数,xx 轴先消掉配对、再把带 ii 的偏移吸收进数据、最后同样落到中位数—— 两分支各自算出 fyf_y 与 fxf_x,相加就是最终答案。