「中位数」输油管道问题
本题为3月13日23上半学期集训每日一题中A题的题解
题面
题目描述
某石油公司计划建造一条由东向西的主要输油管道。该管道要穿过一个有n口油井的油田。从每口油井都要有一条输油管道沿最短路径(或南或北)与主管道相连。如果给定n口油井的位置,及它们的x坐标(东西向)和y坐标(南北向),应如何确定主管道的最优位置,即使各油井到主管道之间的输油管道长度总和最小的位置?证明可规定时间内确定主管道的最优位置。
注:为方便铺设,要求主管道铺设在整数位置点
输入
第一行是油井数n(
接下来n行是油井的位置,每行2个整数x和y(
输出
只有一行
油井到主管道之间的输油管道最小长度总和
样例输入
5
1 2
2 2
1 3
3 -2
3 3
样例输出
6
思路分析
本题是一个经典的中位数模型问题.
首先,由于主管道是东西走向的,所以影响到各油井的分管道总和的只有它们的纵坐标,所以我们可以不用考虑它们的横坐标.
假设y是主管道的y坐标(是个变量),
如果大家还有初中数学数学的记忆的话,会发现这个式子就是在求到数轴上几个整点的距离之和最近的整点的坐标,显然这个坐标就是各个整点坐标的中位数.
考虑到部分同学或许已经忘记了这个规律,这里我们来证明一下.这个问题有很多方法证明,这里我们采用一个比较严谨的方法来证明.
通过简单证明,我们可以得到绝对值三角不等式:
我们前面根据题意得到的那个式子,正好是一组绝对值的和,所以我们可以考虑使用绝对值不等式.这边要注意的是,不等式右边不能得到一个含有变量的式子,这样可能会导致只能取得局部最值(忘了问问高中老师或者数院大佬,我这块学的也不是很清楚),所以我们需要对上面的式子进行变形,以便最后化出来的式子不含自变量y.
如何变形呢?我们只需要交换一半的绝对值中的被减数和减数即可.不过由于最后等号成立需要同号,所以不能随便交换(会导致不存能同号的y),一种(大概也是唯一一种)解决方法是,将
当n是偶数时,我们采用上面的方法对式子进行变形,可得:
当且仅当各个绝对值内数同号时(或全为0时)等号成立,即
可解得
当n是奇数时,情况会稍微有所复杂,显然我们无法通过上面这样直接交换的方式消去y,因为无论如何都会多出来一个y,所以我们需要配凑形式来把式子数量变为偶数,这非常容易(但其实不好想到),我们将每个式子多写一遍,然后整体乘一个二分之一,这样就把绝对值的数量变为偶数了:
然后我们继续采用上面的方法,交换较大的一半(为简便书写,下面的
当且仅当各个绝对值内数同号时(或全为0时)等号成立,即
可解得
综合上面对奇数和偶数的讨论,我们可知:
- 当n(数量)为奇数时,y取得这n个数的中位数时,可以使得总和最小;
- 当n为偶数时,y取
时,可以使得总和最小.
由于偶数的中位数定义为:
有了上面的结论,这题就很好办了,显然x坐标是没用的,我们直接读入各个y坐标,求出它们的中位数,然后计算出每一段距离即可.求中位数的方法非常简单,只需要将各个点的y坐标从小到大排序,然后取中间的数(偶数取中间的两个数,然后算平均值,向下取整)即可.
当然,这题没有要求我们输出主管道的位置,我们也可以直接利用我们前面不等式右边的形式,直接计算结果(当然这也是要排序的,因为我们前面分析那个式子的时候我们假设了
参考代码
先取中位数法
时间复杂度:
空间复杂度:
#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;
}
直接求最小值法
时间复杂度:
空间复杂度:
#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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步