「中位数」输油管道问题
本题为3月13日23上半学期集训每日一题中A题的题解
题面
题目描述
某石油公司计划建造一条由东向西的主要输油管道。该管道要穿过一个有n口油井的油田。从每口油井都要有一条输油管道沿最短路径(或南或北)与主管道相连。如果给定n口油井的位置,及它们的x坐标(东西向)和y坐标(南北向),应如何确定主管道的最优位置,即使各油井到主管道之间的输油管道长度总和最小的位置?证明可规定时间内确定主管道的最优位置。
注:为方便铺设,要求主管道铺设在整数位置点
输入
第一行是油井数n( \(1 \leq n \leq 10000\) )
接下来n行是油井的位置,每行2个整数x和y( \(-10000 \leq x,y \leq 10000\) )
输出
只有一行
油井到主管道之间的输油管道最小长度总和
样例输入
5
1 2
2 2
1 3
3 -2
3 3
样例输出
6
思路分析
本题是一个经典的中位数模型问题.
首先,由于主管道是东西走向的,所以影响到各油井的分管道总和的只有它们的纵坐标,所以我们可以不用考虑它们的横坐标.
假设y是主管道的y坐标(是个变量), \(a_i\) 表示第i个油田的y坐标(是个常量),为方便,假设 \(a_i\) 单调递增,那么各个分管道总和的公式为:
$ | a_1 - y | + | a_2 - y | + | a_3 - y | + ... + | a_n - y | $
如果大家还有初中数学数学的记忆的话,会发现这个式子就是在求到数轴上几个整点的距离之和最近的整点的坐标,显然这个坐标就是各个整点坐标的中位数.
考虑到部分同学或许已经忘记了这个规律,这里我们来证明一下.这个问题有很多方法证明,这里我们采用一个比较严谨的方法来证明.
通过简单证明,我们可以得到绝对值三角不等式: $ |a| + |b| \geq |a + b| $ .这个式子是可以推广到n个绝对值之和的形式(可通过数学归纳法或向量法证明,由于是纯数学知识,所以这里不证明了):
$ |a_1| + |a_2| + |a_3| + ... + |a_n| \geq |a_1 + a_2 + a_3 + ... + a_n| $ ,当且仅当各个数同号时等号成立.
我们前面根据题意得到的那个式子,正好是一组绝对值的和,所以我们可以考虑使用绝对值不等式.这边要注意的是,不等式右边不能得到一个含有变量的式子,这样可能会导致只能取得局部最值(忘了问问高中老师或者数院大佬,我这块学的也不是很清楚),所以我们需要对上面的式子进行变形,以便最后化出来的式子不含自变量y.
如何变形呢?我们只需要交换一半的绝对值中的被减数和减数即可.不过由于最后等号成立需要同号,所以不能随便交换(会导致不存能同号的y),一种(大概也是唯一一种)解决方法是,将 \(a_i\) 较大的一半的被减数和减数交换,这样便可以最终保证能够找到一个y使得等号成立(往下看,下面会给出一个不等式组,观察不等式组的形式你就明白了).
当n是偶数时,我们采用上面的方法对式子进行变形,可得:
$ | a_1 - y | + | a_2 - y | + | a_3 - y | + ... + | a_\frac{n}{2} - y| + | y - a_{\frac{n}{2} + 1}| + | y - a_{\frac{n}{2} + 2} | + | y - a_{\frac{n}{2} + 3} | + ...+ | y - a_n | \geq |a_1 + a_2 + a_3 + ... + a_\frac{n}{2} - a_{\frac{n}{2} + 1} - a_{\frac{n}{2} + 2} - a_{\frac{n}{2} + 3} - ... - a_n|$
当且仅当各个绝对值内数同号时(或全为0时)等号成立,即
\(\begin{cases} a_1 - y \leq 0\\ a_2 - y \leq 0\\ a_3 - y \leq 0\\ ...\\ a_\frac{n}{2} - y \leq 0\\ y - a_{\frac{n}{2} + 1} \leq 0\\ y - a_{\frac{n}{2} + 2} \leq 0\\ y - a_{\frac{n}{2} + 3} \leq 0\\ ...\\ y - a_n \leq 0 \end{cases}\)
可解得 \(a_\frac{n}{2} \leq y \leq a_{\frac{n}{2} + 1}\) ,这便是主管道应该修建位置的范围,在这个范围内任选一个整数(题目要求)均可让总长最小.(另一种全部 \(\geq0\) 是无解的,不信可以自己试试,也正是应为上面这个方程的形式,所以我们仅可采用这种交换的方式,其他方式都会导致y无解)
当n是奇数时,情况会稍微有所复杂,显然我们无法通过上面这样直接交换的方式消去y,因为无论如何都会多出来一个y,所以我们需要配凑形式来把式子数量变为偶数,这非常容易(但其实不好想到),我们将每个式子多写一遍,然后整体乘一个二分之一,这样就把绝对值的数量变为偶数了:
$ \frac{1}{2} \times (| a_1 - y | + | a_1 - y | + | a_2 - y | + | a_2 - y | + | a_3 - y | + | a_3 - y | + ... + | a_n - y | + | a_n - y |) $
然后我们继续采用上面的方法,交换较大的一半(为简便书写,下面的 \(\frac{n}{2}\) 默认向下取整.请注意交换是在 \(a_{\frac{n}{2} + 1}\) 处发生的,所以一个交换,而另一个不交换,最后就会被消掉.想不清楚可以在纸上写一下,看看一半是在哪个位置),可得:
$ \frac{1}{2} \times (| a_1 - y | + | a_1 - y | + | a_2 - y | + | a_2 - y | + | a_3 - y | + | a_3 - y | + ... | a_{\frac{n}{2} + 1} - y| + | y - a_{\frac{n}{2} + 1}| + | y - a_{\frac{n}{2} + 2}| + | y - a_{\frac{n}{2} + 2}| + ...+ | y - a_n | + | y - a_n |) \geq | a_1 + a_2 + a_3 + ... + a_{\frac{n}{2} -1} - a_{\frac{n}{2} + 2} - ... - a_n|$
当且仅当各个绝对值内数同号时(或全为0时)等号成立,即
\(\begin{cases} a_1 - y \leq 0\\ a_2 - y \leq 0\\ a_3 - y \leq 0\\ ...\\ a_{\frac{n}{2} + 1} - y \leq 0\\ y - a_{\frac{n}{2} + 1} \leq 0\\ y - a_{\frac{n}{2} + 2} \leq 0\\ y - a_{\frac{n}{2} + 3} \leq 0\\ ...\\ y - a_n \leq 0 \end{cases}\)
可解得 $ y = a_{\frac{n}{2} + 1} $ ,这便是主管道应该修建位置的位置,且显然这个位置也是个整数.(另一种全部 \(\geq0\) 是无解的,不信可以自己试试)
综合上面对奇数和偶数的讨论,我们可知:
- 当n(数量)为奇数时,y取得这n个数的中位数时,可以使得总和最小;
- 当n为偶数时,y取 $ [a_\frac{n}{2} , a_{\frac{n}{2} + 1}] $ 时,可以使得总和最小.
由于偶数的中位数定义为: \(\frac{a_\frac{n}{2} + a_{\frac{n}{2} + 1}}{2}\) ,显然这个数是在 $ [a_\frac{n}{2} , a_{\frac{n}{2} + 1}] $ 内的,那么当n为偶数时,也可以让y取得这n个数的中位数(这道题要求修建位置为整数,如果 \(\frac{a_\frac{n}{2} + a_{\frac{n}{2} + 1}}{2}\) 不为整数,那么我们可以向下取整,反正只要在这个区间范围内即可).不过,有时候图方便,我们也可以不去求严格的中位数,毕竟只要在这个区间里就好了,我们直接取中间两个数中随便一个即可.
有了上面的结论,这题就很好办了,显然x坐标是没用的,我们直接读入各个y坐标,求出它们的中位数,然后计算出每一段距离即可.求中位数的方法非常简单,只需要将各个点的y坐标从小到大排序,然后取中间的数(偶数取中间的两个数,然后算平均值,向下取整)即可.
当然,这题没有要求我们输出主管道的位置,我们也可以直接利用我们前面不等式右边的形式,直接计算结果(当然这也是要排序的,因为我们前面分析那个式子的时候我们假设了 \(a_i\) 是从小到大的,我们交换的时候也是选择较大的一半交换的).
参考代码
先取中位数法
时间复杂度: \(O(NlogN)\) (排序时间)
空间复杂度: \(O(N)\) (计入输入数据存放空间)
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n;
cin >> n;
// 输入各个点的y坐标
int *a = new int[n];
for (int i = 0; i < n; i++) {
int x; // x坐标用一个空的临时变量读掉
cin >> x >> a[i];
}
// 计算中位数
sort(a, a + n); // 先排序
int mid = a[n >> 1]; // 中位数就是排序后中间那个数(偶数时是中间两个数之一,也可以用.此处下标从0开始,所以不用加一.n >> 1等效于/2)
// 求和
int sum = 0;
for (int i = 0; i < n; i++) {
sum += abs(a[i] - mid);
}
cout << sum << "\n";
delete[] a;
return 0;
}
直接求最小值法
时间复杂度: \(O(NlogN)\) (排序时间)
空间复杂度: \(O(N)\) (计入输入数据存放空间)
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n;
cin >> n;
// 输入各个点的y坐标
int *a = new int[n];
for (int i = 0; i < n; i++) {
int x; // x坐标用一个空的临时变量读掉
cin >> x >> a[i];
}
// 计算最小值
sort(a, a + n); // 先排序
int res = 0;
for (int i = 0; i < (n >> 1); i++) { // 利用双指针技巧(另一个指针是n - i - 1,不用单独维护),使得求奇数时正好会跳掉中间的那个点,不用分类讨论
res += a[i] - a[n - i - 1]; // 头和尾一起算进去
}
cout << abs(res) << "\n"; // 别忘了取绝对值
delete[] a;
return 0;
}
"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德
这里是浙江理工大学22届ACM集训队的成员一枚鸭!
本文首发于博客园,作者:星双子,除了我自己的转载请注明原文链接:https://www.cnblogs.com/geministar/p/zstu23_3_13_A.html