油井问题·分治找中位数BFPTR算法

该博客探讨了三种不同的算法来确定主油管道的最优南北位置,以减小油井喷油管道的长度。方法一采用快速排序,方法二利用随机选择求第k小元素,方法三应用BFPTR算法寻找中位数的“中位数”。所有算法的目标是在线性时间内完成,并避免使用排序。通过测试样例展示了算法的实现和输出结果。
摘要由CSDN通过智能技术生成

题目信息

在这里插入图片描述

主油管道为东西向,确定主油管道的南北位置,使南北向油井喷油管道和最小。要求线性时间完成。1 <= 油井数量 <= 2000000

输入

输入有油井数量行,第 K 行为第 K 油井的坐标 X ,Y 。其中, 0<=X<231,0<=Y<231 。

输出

输出有一行, 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;
}
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhj12399

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值