递归与分治 | 1:选择算法/中位数 —— 例题:油井

耐心整理的精品笔记,花点时间耐心看完,相信会有收获 ^_^ ) 

 

本文目录

解法一:RandomizedSelect算法

一、算法描述

1、分(divide)

2、治(conquer)

二、算法实现

1、partition的实现

2、RandomizedSelect的实现 

完整代码:

解法二:BFPTR算法

1、选取pivot

2、partition的实现

3、BFPTR核心算法

完整代码

具体题目

1、油井问题

 


选择问题:找数组a的 [s, e] 范围内的第k小元素。同时,也就能求出中位数。

  • 解法一:RandomizedSelect算法期望O(n) 时间的选择算法。不稳定,会有比较差的O(n^2)时间出现。
  • 解法二:BFPTR算法最坏情况O(n) 时间的选择算法。理论上是最快的方法,最坏都是线性时间。

两种算法都是通过分治的思想实现的, BFPTR算法可以说是RandomizedSelect算法的一种改进,写起来稍微麻烦一点。


解法一:RandomizedSelect算法

一、算法描述

对数组a的 [s, e] 范围处理:

1、分(divide)

随机找到一个元素,记为pivot。然后将数组按照与pivot的大小关系分为两部分,分别分到pivot的两边。

其实这个分边的操作有点像快排里面的partiton操作,只是我们的pivot是随机选取的。为了方便,我们选取最后一个元素a[e]为pivot,具体操作如下图所示:

 具体如何移动和实现的,之后会具体说。

2、治(conquer)

现在数组已经分好了,pivot所在位置的下标记为pivot_index,那么在pivot左侧(包括pivot)一共有 pivot_index - s + 1 个元素,记为num。然后判断,第k小元素在哪个部分:

  1. 刚好是pivot:直接返回pivot就行
  2. 在pivot左侧:对pivot左侧的元素 —— 数组a的 [s, pivot_index - 1] 递归地找第k小元素
  3. 在pivot右侧:对pivot右侧的元素 —— 数组a的 [pivot_index + 1, e] 递归地找第k - num 小元素

二、算法实现

1、partition的实现

pivot选取为a[e],取两个游标 i、j 分别起始指向下标 s 和 e - 1。分别向中间推进,当 i 指向一个大于 pivot 值时停下,当 j 指向一个小于 pivot 值时停下,然后将两者互换。直到 i > j 结束,然后将 pivot 换到合适位置。

下面是具体操作的图解:

主要注意:结束时的条件、pivot 最终移动到合适的位置。

/* 将数组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 < e - 1)
            i++;
        while (a[j] >= pivot && j > s)
            j--;
        if (i < j)
            swap(a[i], a[j]);
    }
    swap(a[i], a[e]);  //将pivot移动到合适位置
    return i;
}

2、RandomizedSelect的实现 

/* 找数组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);
}

完整代码:

/* 找数组a[s, e]范围内的第k小元素
 * 特殊情况:中位数... */

#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]);
    }
    swap(a[i], a[e]);  //将pivot移动到合适位置
    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 main() {
    int a[10] = {0, 9, 32, -2, 23, 33, 5, 1, 12, 76};
    printf("中位数:%d\n", RandomizedSelect(a, 0, 9, 10 / 2)); //测试输出中位数(小)
    printf("最大数:%d\n", RandomizedSelect(a, 0, 9, 1)); //测试输出最大数
    printf("最小数:%d\n", RandomizedSelect(a, 0, 9, 10)); //测试输出最小数
    printf("%d\n", RandomizedSelect(a, 0, 9, 7)); //其他测试
}

 


解法二:BFPTR算法

其实RandomizedSelect算法会出现比较差的时间的原因就是:由于pivot是随机选取的,paritition分的不够均匀,导致算法会低效。那么BFPTR算法其实就是在这个问题上改进而来的:我们将pivot选取为一个相对来说靠中间的量,使得partition分配后pivot两侧元素个数差别不多。


1、选取pivot

      既然两个算法的差别就是pivot的选取,那么pivot怎么找呢?通过验证(略,具体看算法书),这样子是最合适的:将数组a的 [s, e] 分组,5个一组,最终不足5个的也分到一组。分别每组进行选择排序,同时能找出每组的中位数,将这些中位数抽出,继续在这些中位数中找"中位数"。

  🔺 注意! 

      很多算法书上面描述的是找中位数的中位数,其实有些费解,我看了很多资料才明白,这应该是中文书翻译的锅(吐了)。其实应该这样子表述:找中位数的"中位数",第一个中位数不打引号,是因为确实是每组的中位数的集合;第二个中位数打上了引号,是因为它最后找出的不是真正的中位数,只是一个相对靠中间的数。

  🔺 如何理解呢?

      因为每次将每组的中位数抽出来继续寻找"中位数",这是一个递归的调用。递归的终点是什么?是最终传入的数组只能分为一组(也就是不超过五个元素),那么这时直接返回第一个元素(不要问为啥,就是近似嘛,可以验证这是合理的)。

     所以说,pivot是一个近似的中位数,而非真正的中位数有这两个原因:

  1. 每组中位数的中位数不一定就是整体的中位数
  2. 况且递归的终点也是近似的,随机找的第一个元素嘛

     理解了pivot如何选取,就可以直接上代码了:

/* 在数组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);  //继续递归求解 调到前面的数(每组的中位数)的 "中位数"
}

2、partition的实现

       具体操作和上面描述的RandomizedSelect算法的partition的实现一样,就不重复赘述了,有需要看看上面具体说的。只是pivot是我们的FindMid函数计算出来的。有一点小区别要注意:为了方便移动,pivot需要我们自己最先移动到a[e]位置,最后再移动回合适位置。

      另外有一种特殊情况要注意,就是当序列不需要调整时(pivot为最大元素),应单独考虑。

      具体代码如下:

/* 将数组a的[s, e]范围,按照与pivot的大小关系,划分到pivot两侧 *
 * 返回pivot最终的下标 */
int partition(int a[], int s, int e, int pivot) {
    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, i, e);   //将pivot转到合适位置
        return i;
    }
}

3、BFPTR核心算法

      具体操作和上面描述的RandomizedSelect算法的实现一样是一样的,就不重复赘述了,有需要看看上面具体说的。

/* 找数组的[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);
}

完整代码

// Created by A on 2020/2/26.

/* 找数组a[s, e]范围内的第k小元素
 * 特殊情况:中位数...
 * 最差时间复杂度为线性 O(N)*/

#include <cstdio>
#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) {
    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]);
    }
    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 main() {
    int a[10] = {0, 9, 32, -2, 23, 33, 5, 1, 12, 76};
    printf("中位数:%d\n", BFPTR(a, 0 , 9, 10 / 2)); //测试输出中位数(小)
    printf("最大数:%d\n", BFPTR(a, 0 , 9, 1)); //测试输出最大数
    printf("最小数:%d\n", BFPTR(a, 0 , 9, 10)); //测试输出最小数
    printf("%d\n", BFPTR(a, 0 , 9, 7)); //其他测试
}

具体题目

1、油井问题

 

成绩10开启时间2020年02月25日 星期二 08:55
折扣0.8折扣时间2020年04月30日 星期四 23:55
允许迟交关闭时间2020年04月30日 星期四 23:55

 

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

油井

1<= 油井数量 <=2 000 000

输入要求:

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

输出要求:

输出有一行, N 为主管道最优位置的最小值

注意:用快排做的不给分!!

 

友情提示:可以采用while(scanf("%d,%d",&x,&y) != EOF)的数据读入方式。

 测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 41,969978↵
  2. 26500,413356↵
  3. 11478,550396↵
  4. 24464,567225↵
  5. 23281,613747↵
  6. 491,766290↵
  7. 4827,77476↵
  8. 14604,597006↵
  9. 292,706822↵
  10. 18716,289610↵
  11. 5447,914746↵
以文本方式显示
  1. 597006↵
1秒64M0

本题的x坐标是没有意义的,仔细想想应该知道,最终是需要找出y坐标的中位数才是油管的最佳位置。既然说不能用快排直接找出中位数,那么就可以通过上面的算法来寻找中位数。

我是用BFPTR来实现的,会比较快,完整AC代码如下

#include <cstdio>
#include <algorithm>

using namespace std;
const int N = 2000005;

int a[N];

/* 增序的插入排序
 * 对数组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) {
    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]);
    }
    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 main() {
    int x, y[N], n = 0;
    while (EOF != scanf("%d,%d", &x, &y[n]))
        n++;
    printf("%d\n", BFPTR(y, 0, n - 1, (n + 1) / 2));

}

结果还是比较理想的,算是比较快:

 



end 

欢迎关注个人公众号 鸡翅编程 ”,这里是认真且乖巧的码农一枚。

---- 做最乖巧的博客er,做最扎实的程序员 ----

旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~

 

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值