题目信息
主油管道为东西向,确定主油管道的南北位置,使南北向油井喷油管道和最小。要求线性时间完成。
1 <= 油井数量 <= 2000000
输入
输入有油井数量行,第 K 行为第 K 油井的坐标 X ,Y 。其中, 0<=X<2
31,0<=Y<2
31 。
输出
输出有一行, N 为主管道最优位置的最小值
注意
用排序做的不给分!!
测试样例
41,969978
26500,413356
11478,550396
24464,567225
23281,613747
491,766290
4827,77476
14604,597006
292,706822
18716,289610
5447,914746
597006
解答
方法一·快排
#include <iostream>
#include <algorithm>
using namespace std;
int Y[2000010];
int main()
{
//freopen("E://test.txt", "r", stdin);
int tmpx;
int N = 0;
while (scanf("%d,%d", &tmpx, &Y[N]) != EOF)
{
N++;
}
sort(Y, Y + N);
if (N % 2 != 0)
{//奇数个
printf("%d\n", Y[N / 2]);
}
else
{
printf("%d\n", Y[N / 2 - 1]);
}
return 0;
}
方法二·随机选择求第k小元素
期望为
O(n)
时间的选择算法。不稳定,会有比较差的
O(n^2)
时间出现。
1、分(divide)
随机找到一个元素,记为pivot。然后将数组按照与pivot的大小关系分为两部分,分别分到pivot的两边。
其实这个分边的操作有点像快排里面的partiton操作,只是我们的pivot是随机选取的。为了方便,我们选取最后一个元素a[e]为pivot。
2、治(conquer)
现在数组已经分好了,pivot所在位置的下标记为pivot_index,那么在pivot左侧(包括pivot)一共有 pivot_index – s + 1 个元素,记为num。然后判断,第k小元素在哪个部分:
刚好是pivot:直接返回pivot就行
在pivot左侧:对pivot左侧的元素 —— 数组a的 [s, pivot_index – 1] 递归地找第k小元素
在pivot右侧:对pivot右侧的元素 —— 数组a的 [pivot_index + 1, e] 递归地找第k – num 小元素
/* 找数组a[s, e]范围内的第k小元素
* 特殊情况:中位数... */
#include <iostream>
#include <algorithm>
using namespace std;
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标
* 注:pivot是随机选取的 */
int RandomizedPartition(int a[], int s, int e)
{
int pivot = a[e]; //取最后一个元素为pivot来划分
int i = s, j = e - 1;
while (i < j)
{
while (a[i] < pivot)
i++;
while (a[j] > pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
if (a[i] < pivot) //所有元素都比pivot小,原序列不需要调整
{
return e;
}
else
{
swap(a[e], a[i]); //将pivot移动到合适位置
return i;
}
return i;
}
/* 找数组a[s, e]范围内的第k小元素 */
int RandomizedSelect(int a[], int s, int e, int k)
{
int pivot_index = RandomizedPartition(a, s, e); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return a[pivot_index];
else if (k < num) //在pivot左边找
return RandomizedSelect(a, s, pivot_index - 1, k);
else //在pivot右边找
return RandomizedSelect(a, pivot_index + 1, e, k - num);
}
int Y[2000010];
int main()
{
//freopen("E://test.txt", "r", stdin);
int tmpx;
int N = 0;
while (scanf("%d,%d", &tmpx, &Y[N]) != EOF)
{
N++;
}
if (N % 2 != 0)
{//奇数个
cout << RandomizedSelect(Y, 0, N - 1, N / 2 + 1) << endl;
}
else
{
cout << RandomizedSelect(Y, 0, N - 1, N / 2) << endl;
}
return 0;
}
方法三·BFPTR算法找中位数的“中位数”
其实RandomizedSelect算法会出现比较差的时间的原因就是:由于pivot是随机选取的,paritition分的不够均匀,导致算法会低效。那么BFPTR算法其实就是在这个问题上改进而来的:我们将pivot选取为一个相对来说靠中间的量,使得partition分配后pivot两侧元素个数差别不多。
选取pivot: 既然两个算法的差别就是pivot的选取,那么pivot这样子是最合适的:将数组a的 [s, e] 分组,5个一组,最终不足5个的也分到一组。分别每组进行选择排序,同时能找出每组的中位数,将这些中位数抽出,继续在这些中位数中找”中位数”。
很多算法书上面描述的是找中位数的中位数,其实有些费解。其实应该这样子表述:找中位数的”中位数”,第一个中位数不打引号,是因为确实是每组的中位数的集合;第二个中位数打上了引号,是因为它最后找出的不是真正的中位数,只是一个相对靠中间的数。
因为每次将每组的中位数抽出来继续寻找”中位数”,这是一个递归的调用。递归的终点是什么?是最终传入的数组只能分为一组(也就是不超过五个元素),那么这时直接返回第一个元素。
所以说,pivot是一个近似的中位数,而非真正的中位数有这两个原因:
每组中位数的中位数不一定就是整体的中位数
况且递归的终点也是近似的,随机找的第一个元素
/* 找数组a[s, e]范围内的第k小元素
* 特殊情况:中位数...
* 最差时间复杂度为线性 O(N)*/
#include <iostream>
#include <algorithm>
using namespace std;
/* 增序的插入排序
* 对数组a的[s, e]范围排序 */
void InsertSort(int a[], int s, int e)
{
for (int i = s + 1; i <= e; i++)
{
int temp = a[i]; //取出待插入元素
int t = i; //待插入的位置
/* 当待插入元素 小于 待插入位置的前一个元素 */
while (t > 0 && temp < a[t - 1])
{
a[t] = a[t - 1]; //将前一个元素后移
t--; //待插入位置前移
}
a[t] = temp; //插入合适位置
}
}
/* 在数组a的[s, e]范围内找到一个大致的"中位数" */
int FindMid(int a[], int s, int e)
{
int cnt = 0; //实时记录分组的编号(从零依次)
int i;
/* 五数一组,每组排序后找到中位数,调换到a数组前列 */
for (i = s; i + 4 <= s; i += 5)
{
InsertSort(a, i, i + 4); //排序
swap(a[s + cnt], a[i + 2]); //将中位数调换到a数组前第cnt个
cnt++; //小组增加一个
}
/* 处理剩余元素 */
if (i < e)
{
InsertSort(a, i, e); //排序
swap(a[s + cnt], a[(i + e) / 2]); //将中位数调换到a数组前第cnt个
cnt++;
}
if (cnt <= 1) //递归的终点:只有一个/没有分组的时候
return a[s];
return FindMid(a, s, s + cnt - 1); //继续递归求解 调到前面的数(每组的中位数)的 "中位数"
}
/* 找到数组a的[s, e]范围内 key对应的下标 */
int FindIndex(int a[], int s, int e, int key)
{
for (int i = s; i <= e; i++)
{
if (a[i] == key)
return i;
}
}
/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
* 返回pivot最终的下标 */
int partition(int a[], int s, int e, int pivot)
{//为了方便移动,pivot需要我们自己最先移动到a[e]位置,最后再移动回合适位置
int p = FindIndex(a, s, e, pivot); //找到pivot的下标
swap(a[p], a[e]); //将pivot换到最末端
int i = s, j = e - 1;
while (i < j)
{
while (a[i] <= pivot)
i++;
while (a[j] >= pivot)
j--;
if (i < j)
swap(a[i], a[j]);
}
if (a[i] < pivot) //所有元素都比pivot小,原序列不需要调整
{
return e;
}
else
{
swap(a[e], a[i]); //将pivot移动到合适位置
return i;
}
}
/* 找数组的[s, e]范围内第k小元素 */
int BFPTR(int a[], int s, int e, int k)
{
int pivot = FindMid(a, s, e); //找到一个大致的"中位数"
int pivot_index = partition(a, s, e, pivot); //按照与pivot的大小关系,划分到pivot两侧。返回pivot最终的下标
int num = pivot_index - s + 1; //pivot(包括在内)左侧的元素个数
if (num == k)//第k小元素恰好是pivot
return pivot;
else if (k < num) //在pivot左边找
return BFPTR(a, s, pivot_index - 1, k);
else //在pivot右边找
return BFPTR(a, pivot_index + 1, e, k - num);
}
int Y[11];
int main()
{
freopen("E://test.txt", "r", stdin);
int tmpx;
int N = 0;
while (scanf("%d,%d", &tmpx, &Y[N]) != EOF)
{
N++;
}
if (N % 2 != 0)
{//奇数个
cout << BFPTR(Y, 0, N - 1, N / 2 + 1) << endl;
}
else
{
cout << BFPTR(Y, 0, N - 1, N / 2) << endl;
}
return 0;
}