洛谷 P10452 货仓选址 题解

题解

题意概述:

  • 找到一个点,使其到达每家商店的距离之和最小。

思路历程:

  • 最开始没想那么多,直接暴力枚举,但是在第三个测试点上就超时了,看数据大小,\(1 \le N \le 10^5\)\(0 \le A_i \le 40000\),(还没有养成看数据大小的习惯)那么就是\(10^9\)的循环次数,会超时

  • 然后我又假如了几个限制条件,如只从min(最小的坐标点)遍历到max,假如count>ml,直接break,但是这些条件还是没有从根本上优化\(10^9\)的复杂度

  • 我也觉得应该就是中间的某个点是最短距离和的那个点,但是我又疑惑于怎么表达中间那个点,用什么区间来遍历.是算min和max的平均数,还是算a[mid],当时我觉得a[mid]会受到数据分布左右不平衡的影响,所以应该多遍历a[mid]左右的区间,但是这个区间的范围我也不知道怎么限定.我还想着能不能用其他什么算法来做,但也没做出来

  • 最后,我才知道这道题求的就是所有坐标点的中位数

    下面是AI给出的证明方法

    1. 几何直观:分组博弈法

    假设我们将所有的商店坐标按从小到大排序:\(A_1 \le A_2 \le \dots \le A_N\)

    我们把最外层的两家商店 \(A_1\)\(A_N\) 看作一组。

    • 如果要让货仓到这两家商店的距离之和 \(|x - A_1| + |x - A_N|\) 最小,货仓 \(x\) 应该建在哪里?
    • 根据几何性质,只要 \(x\)\([A_1, A_N]\) 这个闭区间内,距离之和恒等于 \(A_N - A_1\)(即这段线段的长度)。
    • 如果 \(x\) 跑到了 \(A_1\) 左边或 \(A_N\) 右边,距离之和一定会大于线段长度。

    接下来,我们看次外层的两家商店 \(A_2\)\(A_{N-1}\)

    • 同理,要让 \(|x - A_2| + |x - A_{N-1}|\) 最小,\(x\) 必须落在 \([A_2, A_{N-1}]\) 之间。

    结论:

    为了让总距离之和最小,\(x\) 必须尽可能满足所有这些“嵌套区间”的约束。

    • \(N\) 为奇数时: 所有区间的交点只有一个,就是最中间的那个点 \(A_{(N+1)/2}\)

    • \(N\) 为偶数时: 所有区间的交点是一个闭区间 \([A_{N/2}, A_{N/2+1}]\)。在这个区间内的任意一点(包括端点)都能达到最小值。通常为了方便,我们取 \(A_{N/2}\) 或者中位数。

    • 中位数恰恰是应对这种“不平衡”最强大的工具。

      我们可以用“拉力赛”来想象:

      • 假设你站在数轴上的某个点 \(x\)
      • 每一个位于你左边的商店,都想把你往左拉;每一个位于你右边的商店,都想把你往右拉。
      • 如果你向右移动 1 厘米:
        • 你离左边所有商店的距离都增加了 1 厘米。
        • 你离右边所有商店的距离都减少了 1 厘米。
      • 只要你左右两边的商店数量不等,比如左边有 10 家,右边有 20 家,那么你往右走,总距离一定会减少(减少了 \(20-10=10\) 厘米)。
      • 只有当你走到一个点,使得“左边的商店数 = 右边的商店数”时,你无论往哪边走,总距离都会增加。

      这就是为什么点的具体数值不重要,点的个数才重要

  • 平均数(Mean) 最小化的是距离的平方和 \(\sum (x - A_i)^2\),这是最小二乘法的原理。

    中位数(Median) 最小化的是绝对距离之和 \(\sum |x - A_i|\),这在统计学中被称为 \(L_1\) 范数最小化。

代码:

#include <bits/stdc++.h>

using i64 = long long;

void solve(){
int n;
std::cin >> n; 
std::vector<int> a(n,0);
for(int i = 0; i < n; ++i){
	std::cin >> a[i];
}
std::sort(a.begin(),a.end());
int count = 0;
if(n & 1){
	int temp = a[n >> 1];
	for(int i = 0; i < n; ++i){
		count += std::abs(temp - a[i]);
	}
}
else{
	for(int i = 0; i < n / 2; ++i){
		count += a[n - 1 - i] - a[i];
	}
}
std::cout << count;
}

int main(){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

solve();

return 0;
}



  • 其实偶数中的配对思路奇数也受用,奇数中可以看成中间的那两个数是一个数了

  • 更简洁的版本

    #include <bits/stdc++.h>
    
    using i64 = long long;
    
    void solve(){
        int n;
        std::cin >> n;
        
        std::vector<int> a(n, 0);
        
        
        for(int i = 0; i < n; ++i){
            std::cin >> a[i];
        }
        std::sort(a.begin(), a.end());
        int count = 0;
        for(int i = 0; i < n / 2; ++i){
            count += a[n - 1 - i] - a[i];
        }
        
        std::cout << count;
    }
    
    int main(){
        std::ios::sync_with_stdio(false);
        std::cin.tie(nullptr);
    
        solve();
    
        return 0;
    }
    

新学知识点

位运算(注意运算优先级)

& 按位与

  • 只有对应的两个二进位均为 \(1\) 时,结果位才为 \(1\),否则为 \(0\)

  • 用法:

    • 判断奇偶:

      n & 1:计算机存储整数时,偶数的二进制最后一位必然是 \(0\),奇数的最后一位必然是 \(1\)

      所以当奇数时n & 1为真

      偶数时n & 1为假

>> 按位右移

  • 在处理正整数时,右移 \(k\) 位在数学上完全等同于除以 \(2^k\) 并向下取整
  • 用法:(n + 1) >> 1等价于\((n + 1) / 2\)

总结启发

  • 数学在算法竞赛中的作用???已经做到了一些题目与数学强相关或者结合数学有新方法
posted @ 2026-03-28 16:03  XZXZZX  阅读(1)  评论(0)    收藏  举报