分治法找两个序列中找第k小

两个有序数序列中找第k小


Abstrct

在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范型。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)。

另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。

以上这段话引自维基百科,下面我们根据分治算法的思想,构造求解“两个有序数序列中找第k小”问题的思路。


Problem Description

已知两个已经排好序(非减序)的序列X和Y,其中X的长度为m,Y长度为n,
现在请你用分治算法,找出X和Y的第k小的数,算法时间复杂度为O(max{logm, logn})。

此题请勿采用将序列X和Y合并找第k小的O(m+n)的一般方法,要充分利用X和Y已经排好序的这一特性。


Input

第一行有三个数,分别是长度m、长度n和k,中间空格相连(1<=m,n<=100000; 1<=k<=m+n)。
第二行m个数分别是非减序的序列X。第三行n个数分别是非减序的序列Y。


Output

序列X和Y的第k小的数。


Sample Input

5 6 7
1 8 12 12 21 
4 12 20 22 26 31

Sample Output

20

Hint

提示陆续写上来,不着急,先自行思考和讨论……


Solution

如果不用分治算法,我们可以直接将X序列Y序列合并,然后for循环一遍,就能够找到第k小的元素,时间复杂度O(m+n)

如果采用分治方法,我们应该怎么做呢?

在这里插入图片描述

首先我们假设:序列X为X [XBegin…XEnd],而序列Y为Y [YBegin…YEnd]

将序列X和Y都均分2段,即取X序列中间位置为 Xmid (Xmid =( xBegin+xEnd ) / 2 ),这样就将序列X分为X [XBegin…Xmid]和X [Xmid+1…XEnd] 两部分。同理,也取序列Y中间位置为Ymid,分为Y [YBegin…Ymid]和Y [Ymid+1…YEnd] 两部分。

记录序列X左段和序列Y左段元素个数合计为halfLen,即halfLen = Xmid-XBegin+1+Ymid-YBegin+1

此时需要比较X[Xmid]Y[Ymid]

  1. 当X[Xmid] < Y[Ymid]时,在合并的数组中,原X[xBegin…Xmid]所有元素一定在Y[Ymid]的左侧,
    (1) 若k < halfLen,则此时第k大的元素一定不会大于Y[Ymid]这个元素,
    故以后没有必要搜索 Y[Ymid…YEnd]这些元素,可弃Y后半段数据。
    此时只需递归的对X序列+Y序列的前半段,去搜索第k小的数。

    (2) 若k >= halfLen,则此时第k大的元素一定不会小于X[Xmid]这个元素,
    故以后没有必要搜索 X[XBegin…Xmid]这些元素,可弃X前半段数据。
    此时只需递归的对X序列的后半段+Y序列,去搜索第 k-(Xmid-XBegin+1)小的数。

  2. 当X[Xmid] >= Y[Ymid]时,在合并的数组中,原Y[YBegin…Ymid]的所有元素一定在X[Xmid]的左侧,
    (1) 若k < halfLen,则此时第k大的元素一定不会大于X[Xmid]这个元素,
    故以后没有必要搜索 X[Xmid…xEnd]这些元素,可弃X后半段数据。
    此时只需递归的对X序列的前半段+Y序列,去搜索第k小的数。

    (2) 若k >= halfLen,则此时第k大的元素一定不会小于Y[Ymid]这个元素,
    故以后没有必要搜索 Y[YBegin…Ymid]这些元素,可弃Y前半段数据。
    此时只需递归的对X序列+Y序列的后半段,去搜索第 k-(Ymid-YBegin+1)小的数。

至于递归的边界如下:

if (XBegin > XEnd) return Y[YBegin + k - 1];  //X序列为空时,直接返回Y序列的第k小元素。
if (YBegin > YEnd) return X[XBegin + k - 1];  //Y序列为空时,直接返回X序列的第k小元素。

在这里插入图片描述

我们再回顾一下以上思路~


首先我们假设:序列X为X [XBegin...XEnd],而序列Y为Y [YBegin...YEnd]。

将序列X和Y都均分2段,即取X序列中间位置为 Xmid (Xmid =( xBegin+XEnd ) / 2 ),这样就将序列X分为X [XBegin...Xmid]和X [Xmid+1...XEnd] 两部分。同理,也取序列Y中间位置为Ymid,分为Y [YBegin...Ymid]和Y [Ymid+1...YEnd] 两部分。

记录序列X左段和序列Y左段元素个数合计为halfLen,即halfLen = Xmid-XBegin+1+Ymid-YBegin+1。

此时需要比较X[Xmid]和Y[Ymid]1. 当X[Xmid] < Y[Ymid]时,在合并的数组中,原X[xBegin...Xmid]所有元素一定在Y[Ymid]的左侧,
   (1) 若k < halfLen,则此时第k大的元素一定不会大于Y[Ymid]这个元素,
         故以后没有必要搜索 Y[Ymid...YEnd]这些元素,可弃Y后半段数据。
         此时只需递归的对X序列+Y序列的前半段,去搜索第k小的数。
 
   (2) 若k >= halfLen,则此时第k大的元素一定不会小于X[Xmid]这个元素,
         故以后没有必要搜索 X[XBegin...Xmid]这些元素,可弃X前半段数据。
         此时只需递归的对X序列的后半段+Y序列,去搜索第 k-(Xmid-XBegin+1)小的数。
 
2. 当X[Xmid] >= Y[Ymid]时,在合并的数组中,原Y[YBegin...Ymid]的所有元素一定在X[Xmid]的左侧,
   (1) 若k < halfLen,则此时第k大的元素一定不会大于X[Xmid]这个元素,
         故以后没有必要搜索 X[Xmid...xEnd]这些元素,可弃X后半段数据。
         此时只需递归的对X序列的前半段+Y序列,去搜索第k小的数。
 
   (2) 若k >= halfLen,则此时第k大的元素一定不会小于Y[Ymid]这个元素,
         故以后没有必要搜索 Y[YBegin...Ymid]这些元素,可弃Y前半段数据。
         此时只需递归的对X序列+Y序列的后半段,去搜索第 k-(Ymid-YBegin+1)小的数。


至于递归的边界如下:

if (XBegin > XEnd) return Y[YBegin + k - 1];  //X序列为空时,直接返回Y序列的第k小元素。
if (YBegin > YEnd) return X[XBegin + k - 1];  //Y序列为空时,直接返回X序列的第k小元素。

时间复杂度为 O(max{log n, long m})

在这里插入图片描述


(1)我们来看一下X[Xmid]<Y[Ymid]时,k<halfLen的例子:


在这里插入图片描述


(2)我们来看一下X[Xmid]<Y[Ymid]时,k>=halfLen的例子:

在这里插入图片描述

(3)我们来看一下X[Xmid]>=Y[Ymid]时,k<halfLen的例子:

在这里插入图片描述


(4)我们来看一下X[Xmid]>=Y[Ymid]时,k>=halfLen的例子:

在这里插入图片描述


分治核心伪代码如下:



if X[Xmid]<Y[Ymid]:
	if k<halfLen:
		将Y右区间丢弃,在序列X+序列Y左区间中分治,寻找第k小的元素
	else:
		在序列X右区间+序列Y中分治,寻找第k-(Xmid-XBegin+1)小的元素
else:
	if k<halfLen:
		将X右区间丢弃,在序列X左区间+序列Y中分治,寻找第k小的元素
	else:
		在序列X+序列Y右区间中分治,寻找第k-(Xmid-XBegin+1)小的元素

Code

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;
typedef long long ll;

const int maxn=1e5+100;
const int N=1e6+100;

int n,m,T;
int X[N],Y[N];
int k;

int split(int XBegin,int XEnd,int YBegin,int YEnd,int k) //分治算法
{
    if(XBegin>XEnd)return Y[YBegin+k-1]; // X序列为空时,直接返回Y序列的第k小元素
    if(YBegin>YEnd)return X[XBegin+k-1]; // Y序列为空时,直接返回X序列的第k小元素

    int Xmid=(XBegin+XEnd)>>1; // 求X序列中间位置
    int Ymid=(YBegin+YEnd)>>1; // 求Y序列中间位置

    int halfLen=(Xmid-XBegin+1)+(Ymid-YBegin+1); // 求X[XBegin...Xmid]与Y[YBegin...Ymid]共有多少元素

    if(X[Xmid]<Y[Ymid]) // 此时X[XBegin...Xmid]必然在Y[mid]的左边
    {
        if(k<halfLen) // 第k小的元素必然不大于Y[mid],因此可以丢弃Y[Ymid+1...YEnd]区间
            return split(XBegin,XEnd,YBegin,Ymid-1,k);
        else // 第k小的元素必然不小于X[mid]。此时可以丢弃X[XBegin...Xmid]区间,等价于寻找第k-(Xmid-XBegin+1)小元素
            return split(Xmid+1,XEnd,YBegin,YEnd,k-(Xmid-XBegin+1));
    }
    else
    {
        if(k<halfLen) // 第k小的元素必然不大于X[mid],因此可以丢弃X[Xmid+1...XEnd]区间
            return split(XBegin,Xmid-1,YBegin,YEnd,k);
        else // 第k小的元素必然不小于Y[mid]。此时可以丢弃Y[YBegin...Ymid]区间,等价于寻找第k-(Ymid-YBegin+1)小元素
            return split(XBegin,XEnd,Ymid+1,YEnd,k-(Ymid-YBegin+1));
    }

}



void solve()
{
    scanf("%d%d%d",&n,&m,&k);

    for(int i=0; i<n; i++)
        scanf("%d",X+i);

    for(int i=0; i<m; i++)
        scanf("%d",Y+i);


    int ans=split(0,n-1,0,m-1,k);

    printf("%d\n",ans);

}


int main()
{
#ifdef LOCAL
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
#endif
    solve();

    /*
    *
    *  ┏┓   ┏┓+ +
    * ┏┛┻━━━┛┻┓ + +
    * ┃       ┃
    * ┃   ━   ┃ ++ + + +
    *  ████━████+
    *  ◥██◤ ◥██◤ +
    * ┃   ┻   ┃
    * ┃       ┃ + +
    * ┗━┓   ┏━┛
    *   ┃   ┃ + + + +Code is far away from  
    *   ┃   ┃ + bug with the animal protecting
    *   ┃    ┗━━━┓ 神兽保佑,代码无bug 
    *   ┃        ┣┓
    *    ┃        ┏┛
    *     ┗┓┓┏━┳┓┏┛ + + + +
    *    ┃┫┫ ┃┫┫
    *    ┗┻┛ ┗┻┛+ + + +
    */
}



最后感谢小伙伴们的学习噢~

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

__Wedream__

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

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

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

打赏作者

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

抵扣说明:

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

余额充值