写在前面:
本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。
不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。
同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。
加油!
类似题指路:力扣O41
成绩 | 10 | 开启时间 | 2021年09月3日 星期五 11:30 |
折扣 | 0.8 | 折扣时间 | 2021年09月7日 星期二 23:00 |
允许迟交 | 否 | 关闭时间 | 2021年10月10日 星期日 23:00 |
Description
想必同学们已经深刻体会到军训的辛苦了,与此同时我们也应该感谢教官们的辛苦付出。现在请你帮助教官小张解决如下问题吧!
马上集合了,但现在同学们分布在操场各个位置。共有n位同学,他们在操场的位置可以用表示。现在小张想知道对于每一个i,前i个同学集合应该如何选择位置使得所有同学走的总曼哈顿距离最短。
Input
本题为多组数据。
第一行一个数,表示数据组数。
每组数据第一行一个数,表示同学个数。
接下来行,每行两个数,表示每个同学位置。
数据保证所有组数据。
Output
每组数据第一行输出数据组数的信息(见样例)。
之后行,每行一个数,保留4位小数,表示前位同学集合到同一个点最少走的曼哈顿距离。
Notes
两点曼哈顿距离为。
测试用例 1 | 以文本方式显示
| 以文本方式显示
| 1秒 | 64M |
题意分析:
找最小曼哈顿距离之和的集合点。曼哈顿距离不再解释, 题目里就有。
这题我们首先可以注意到,曼哈顿距离的x和y的计算应该是互相独立、互不干扰的,所以我们可以把两者单独拉出来算。那么问题就被简化为了一维的:在数轴上寻找一个点,使得若干个点与其的距离之和最小,实际上这个距离之和就是这样一个函数:
高中数学好的同学其实一眼就知道这是什么玩意了,要是还是看不出来呢,那我画个图给你看。
以上两幅图分别是{}为{1,2,4,7,11}和{1,2,3,4,7,11}的情况。
用数学方法不难分析出,当总元素个数为奇数时,的最小值点为所有元素的中位数,最小值为中位数与其他元素之差的和;当总元素个数为偶数时,的最小值点取遍区间[a,b],其中a,b是所有元素中间的两个,最小值为a,b任一个与其他所有元素(包括没选上的那一个a或者b)之差的和。
这题既然要求我们对于每输入一个数据,就更新一个输出,那我们就需要一个能动态取出中位数的容器,并且取中位数的时间得控制在常数级别。那要怎么做呢?
我们回想一下,如果我们需要动态存取一个容器中最大的那个值,我们会用什么?答案是堆,也就是优先队列(名词解释:堆与优先队列)。同时,想要动态存取最小值,也可以用优先队列。那动态存取中位数该怎么办呢?
聪明的人已经想到了,最大的那一半,其中的最小值,或者最小的那一半其中的最大值,不就是我们要的中位数吗(二者至少其中之一是)!所以我们的目标就是,构建一个大根堆,用于保存较小的那一半;构建一个小根堆,用于保存较大的那一半;同时既然是“各一半”,我们就要尽量保证两个根堆大小相等——最多差一个。
于是,整个思路就非常的自然了。
详情看伪代码。
伪代码:
这里只写中位数机的伪代码,写出中位数机以后后续的步骤就不难了。
构建一个大根堆,一个小根堆;
//整个过程中,我们保证大根堆的大小至多比小根堆大1,当然,反过来定义也行,看你喜欢
读入新数据的操作:
如果大根堆和小根堆一样大,将数据先压入小根堆,同时把小根堆堆顶弹出,压入大根堆;
//想一想,为什么要这么做?——目的是保证小根堆存放的皆为“较大的”那一半,大根堆存放“较小的那一半”,如果直接压入大根堆,并不能保证新数据属于较小的那一半,于是先压入小根堆,再把小根堆最小的那个取出还给大根堆,不用担心压入小根堆的那个数据过小导致破坏这个过程——如果过小,那之后会马上被弹出
如果大根堆更大,先将数据压入大根堆,再将大根堆堆顶弹出,压入小根堆;
取中位数的操作:
//我们不是严格取中位数,当中间有两个数时,我们任取其一就行,如果要严格取中位数,简单改造就行,留作读者自行思考
无论两个堆大小关系如何,都返回大根堆顶;
//当然,你要分类讨论也行,大根堆大时返回大根堆顶,一样大时返回二者的平均数,然而这样对这题来说反而不好,理由一会说
注意事项:
为什么说“正经地返回中位数”不好呢?这不是鼓励我们投机取巧吗?
实际上,如果我们正经地返回中位数,有可能会涉及到浮点数的计算(比如中间数字是3和4),而double类型的计算相较于long long是非常之慢的,超时就在一念间。既然这题全部输入都是整型,并且我们也证明了我们所需要的点就是所有点其中之一,也是一个整型,那自然是能不引入浮点就不引入浮点。
然而,题目最终要求我们输出浮点数,那咋办啊?
实际上,你会发现,不论怎么算,最后输出出来的所谓“浮点数”,都没有小数部分(因为我们选定的集合点是一个整数,那接下来必定只有整数的加减法,结果自然不会有小数部分),我认为这是题目设计的疏漏。(当然,到了你们这一届可能题目已经改了)
其他的注意事项就比如,要开long long这样的老生常谈了,不再赘述。
贴代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f;
const double EPS = 1e-8;
#define __MAX 100010
class MedianFinder{ //中位数机
private:
priority_queue<ll, vector<ll>, less<ll>> maxHeap;
priority_queue<ll, vector<ll>, greater<ll>> minHeap;
public:
MedianFinder(){}
void addNum(ll num){
if(maxHeap.size() == minHeap.size()){
minHeap.push(num);
ll val = minHeap.top();
minHeap.pop();
maxHeap.push(val);
} else{
maxHeap.push(num);
ll val = maxHeap.top();
maxHeap.pop();
minHeap.push(val);
}
}
ll getMedian(){
if(maxHeap.size() == minHeap.size()){
return ((maxHeap.top() + minHeap.top()) / 2);
} else{
return (maxHeap.top());
}
}
~MedianFinder(){}
};
int main(){
///ifstream infile("input.txt", ios::in);
///ofstream outfile("output.txt", ios::out);
int t;
cin >> t;
int n;
ll tx, ty;
for(int i = 1; i <= t; i++){
MedianFinder x;
MedianFinder y;
scanf("%d",&n);
printf("Case: %d\n",i);
ll cur = 0;
int mx;
int my;
for(int j = 0; j < n; j++){
scanf("%lld %lld",&tx, &ty);
if(j == 0){
mx = tx;
my = ty;
}
x.addNum(tx);
y.addNum(ty);
if(j % 2 == 0){
mx = x.getMedian();
my = y.getMedian();
cur += abs(mx - tx) + abs(my - ty);
} else{
cur += abs(mx - tx) + abs(my - ty);
mx = x.getMedian();
my = y.getMedian();
}
printf("%lld.0000\n",cur); //假装输出浮点数
}
}
return 0;
}