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

  • Post author:
  • Post category:其他




题目信息

在这里插入图片描述

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

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;
}



版权声明:本文为zhj12399原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。