两个有序数序列中找第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]:
-
当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)小的数。 -
当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
* ┃ ┣┓
* ┃ ┏┛
* ┗┓┓┏━┳┓┏┛ + + + +
* ┃┫┫ ┃┫┫
* ┗┻┛ ┗┻┛+ + + +
*/
}
最后感谢小伙伴们的学习噢~