洛谷 P10452 货仓选址 题解

2 阅读5分钟

题解

题意概述:

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

思路历程:

  • 最开始没想那么多,直接暴力枚举,但是在第三个测试点上就超时了,看数据大小,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

总结启发

  • 数学在算法竞赛中的作用???已经做到了一些题目与数学强相关或者结合数学有新方法