算法 {多路归并,二路归并,第K大数}

二路归并

多路归并 是 二路归并 的拓展, 因此先从二路归并开始;

二路归并是一种(算法思想), 并不是一个具体的算法, 他的实现不是唯一的


给定两个序列: A = {a1, a2, a3, ...}B = {b1, b2, b3, ...}, 长度分别为: LA 和 LB
将这两个序列, 进行(复合, 即归并操作), 形成一个新的序列: C
然后, 求序列C里的 第K大数

(归并操作), 大致分为2类: (1, 并集) (2, 元素复合)

归并操作–并集

对于(并集)操作, 是将A,B序列的元素, 都原封不动的, 放到新序列C
即, 新序列的长度是LA + LB, C = {A} + {B};
此时求C的(第K小数)

限制
La, Lb可以很大, 甚至是你无法存储下来的; (比如, 他是个等差/等比数列)
但是, K不大, 比如是1e6

也就是, C序列的大小 可以非常大, 我们无法获得C序列的每个元素; 但是, 答案只关注前K小/大 的元素, 而K不大

这里以求(第K小数)为例子, 求(第K大数)是一样的;

对于LA, LB不大, 我们首先将A, B序列 进行sort排序; (如果LA, LB很大, 此时他是等差/等比数列, 意味着它已经是排序了的)

即, 此时有:
A = {a1, a2, a3, ..., aLA} 递增
B = {b1, b2, b3, ..., bLB} 递增


暴力

算法1–暴力 O(2K*logK)
定理1
可以证明, C的 (第K小数), 一定是 (A的前K小数) 和 (B的前K小数) 中的某个数, 即在这K + K个元素中;
… 一定不会是a(K + i), 因为该元素 在C序列中, 至少是第K + i小的数

因此, 即使LA, LB很大, 没有关系, 我们只关注: {a1, ..., aK} 和 {b1, ..., bK}K + K个元素; 而K很小
即此时C = {a1, ..., ak, b1, ..., bk}

直接将这K + K个元素, 进行sort排序, 返回第K小数

二叉树

算法2–二叉树 O(K) (常用)
该算法思想, 是二路归并的核心思路, 涉及二路归并的众多应用

此时需要预备一个前提知识:
对于排序了的A, B序列, 其对应的C序列的 最小数, 一定是: min( a1, b1)
其实也是上面的定理1的特例;

第一小数, 面临的AB序列是: {a1,...} 和 {b1, ...} (总共K+K个数), 因此, 它的值为min( a1, b1)

第二小数, 它面临的AB序列是: {a2, ...} 和 {b1, ...}{a1, ...} 和 {a2, ...} 它有两种可能, 因为第一小数有两种可能, (但总共都是K+K-1个数)
假如(第一小数的结果为a1), 则此时, 第二小数面临的AB序列是: {a2, ...} 和 {b1, ...},
则此时第二小数的结果为: min( a2, b1) (等价于在{a2, ...} 和 {b1, ...}最小数, 即min( 两个队头元素: a2, b1)

… 因为, 对于长度为N的序列, 它的(第K小数), 就等价于:
… 将序列的(前K - 1小数)都从序列中删除掉 后的 长度为N - K + 1的序列的 最小数

因此, 从最初的集合{a1, ...} 和 {b1, ...}, 我们每次 根据队头两个元素, 获取集合的 最小数 (即min( ai, bj))
然后, 将该最小数, 从其所在集合中, 删除, 得到新的集合{ai, ...} 和 {bj, ...}
然后, 重复执行该过程;

即, 最初AB集合有: LA + LB个元素,
获取第一小数后, 还有LA + LB - 1个元素
获取第二小数后, 还有LA + LB - 2个元素
获取第三小数后, 还有LA + LB - 3个元素
… 当我们在获取(第K小数)时, 其实就是在{ai, ...} 和 {bj, ...}的(这LA + LB - K + 1个数)中, 找到它的 (最小数)

为什么这个算法叫做 (二叉树)呢?
对于每一个(第K小数), 它有两种可能min( ai, bj) (即两个队头), 如果你将它的(所有可能)都枚举出来, 画出来
会发现, 他会形成一个 (二叉树) 形态;

具体算法:
队头的删除, 不需要真的删除, 用双指针即可, 维护a_indb_ind, 都是从0开始, 进行K循环,
每一次循环, 得到C序列的第i小数, 自然到了最后一次循环, 就得到了第K小数

a_ind = 0, b_ind = 0;
for( int th = 0; th < K; ++th){ //< th是英文的后缀, 是`第`排名的意思
    if( a_ind >= LA){
        ans[ th] = B[ b_ind ++];
    }
    else if( b_ind >= LB){
        ans[ th] = A[ a_ind ++];
    }
    else{
        if( A[ a_ind] <= B[ b_ind]){
			ans[ th] = A[ a_ind ++];
        }
        else{
			ans[ th] = B[ b_ind ++];
        }
    }

归并操作–元素复合

对于(元素复合)操作, 是从每个序列里 都各取一个元素, 即形成一个pair (Ai, Bj)
此时, C序列的大小 是(指数级别的), LA * LB个元素

一个pair( Ai, Bj), 他的值, 可以是(加法) 可以是(乘法) …
我们以加法为例, C = {a1+b1, a1+b2, ..., a1+bLB, a2+b1, a2+b2, ..., a2+bLB, ..., aLA+bLB}LA * LB个元素

此时, 求他的(第K小/大数)

我们以求(第K小数)为例:

其实大致和(上面的 归并操作–并集) 差不多;
首先, 将A, B序列排序


暴力

算法1–暴力 O(K^2 * log(K*K))
对于第K小数Ai + Bj, 则一定有: i <= K, j <= K
即, 他只会涉及到: A序列的前K个数 和 B序列的前K个数 的复合
Ai + Bj (i,j <= K)排序, 得到第K个


二叉树

算法2–二叉树 O(K * logK)
对于A, B序列, 其C序列中 最小数, 一定是 (AB序列开头所组成的), 即: A1 + B1这个数;
第二小数, 即C序列中 将(第一小数)从C序列中删除掉的, 最小数;

但是, 这里的 和 上面的(二叉树算法) 的不同点: 上面(并集操作)中, C序列的元素 和 A/B序列里的元素, 是一样的
即, C的最小数 就是 A的最小数 或 B的最小数;
但是在这里, C的最小数是A1 + B1, 他和 A里面的元素, 是不同的!!! 他是一种(复合)
即, (删除C序列的最小数) 并不等价于 (删除A/B序列的最小元素)

但也有相似点, 都是通过: 从C序列中, 删除当前(最小数)后, 然后再求(最小数), 就得到了 (第二小数)

C序列, 从下面矩形的角度来看

(1,1)  (1,2)  (1,3) ... (1, K)
(2,1)  (2,2)  (2,3) ...
(3,1)  (3,2)  (3,3) ...
...
(K, 1) ... 

C序列, 一共K * K个元素 (不必要是LA * LB, 最多用到 每个序列的前K个元素);
每个(i, j)表示, 他的值是: Ai + Bj

这个矩阵的规律是:
由于A, B序列是递增的, 因此: (i, j) 大于等于 上(i - 1, j) 和 左(i, j - 1);
最小的数, 一定是在 (左上角)
更重要的性质是: 对于第K小数(i, j), 因为所有的(<=i, <=j) 都是小于等于 (i, j)
… 因此, 所有的(<= i, <= j)元素 (即位于该第K小数(i, j)的 左或上方的元素), 一定是第< K小数
即, 所有<= K小的数, 即这些点, 他们在该矩形里 的分布, 有以下规律:
… 含有(1, 1)
… 任何点 的 (所有左上角元素), 一定也在 该图形里;
举例:

a b c d
e f g h
i j k l

所有 前K小的数, 在矩形里的图形 不可以是: a b f (因为, f的左侧e不在该图形里)
a e f是不可以的 (因为f的上侧b不在该图形里)
a b c d是可以的; a b c e i是可以的; a b c e f i是可以的;

假设, 前K小数 是: a b c e f, 我们如何求 第K + 1小数呢?
刨去a b c e f后的矩形是:

_ _ _ d
_ _ g h
i j k l

此时, 这个(残缺矩形)的 最小数, 一定是: min( d, g, i) (所有行的队头元素)
如果两个行的长度相同, 则选择靠上行的队头元素
比如, 最初这个完整的矩形, 不需要维护a, e, i; 只需维护a即可;


此时做法有2种:

堆维护

做法一: 堆维护
我们维护一个heap, 存{sum, i, j}, 表示: Ai + Bj = sum, heap以sum从小到大排序
最初放入{A1 + B1, 1, 1};
进行K次循环, 第a次循环, 把heap.top(): {sum, i, j}拿出, 就意味着, sum是第a小数
拿出之后, 再将{A(i+1) + Bj, i+1, j}{Ai + B(j+1), i, j+1}, 两个元素放入
但是, 要注意:
… 新放入这两个元素, 并不一定是 (所有行的队头元素), 我们想的是(维护所有行的队头元素);
… … 比如上面矩形为例, 我们会把j放入队列, 但他不是队头元素; 但也不要紧, 总之heap里维护的, 一定是有 (所有行的队头元素)
… 我们会重复放入同一个(元素)多次!!! 比如, g会让h入heap, d也会让h入heap
… … 因此, 必须防止重复, 如果放入过heap, 就不要再放;

set< pair< int, int> > sett;
using Item_ = tuple< int, int, int>;
priority_queue< Item_, vector< Item_>, greater< Item_> > heap;
heap.push( {A[ 0] + B[ 0], 0, 0});
sett.insert( { 0, 0});
FOR_( th, 0, N - 1, 1){
    auto sum = GET_( heap.top(), 0);
    auto a_ind = GET_( heap.top(), 1);
    auto b_ind = GET_( heap.top(), 2);
    heap.pop();
    //==
    Ans[ th] = sum;
    //--
    if( a_ind + 1 < N){
        if( sett.find( { a_ind + 1, b_ind}) == sett.end()){
            sett.insert( { a_ind + 1, b_ind});
            heap.push( { A[ a_ind + 1] + B[ b_ind], a_ind + 1, b_ind});
        }
    }
    if( b_ind + 1 < N){
        if( sett.find( { a_ind, b_ind + 1}) == sett.end()){
            sett.insert( { a_ind, b_ind + 1});
            heap.push( { A[ a_ind] + B[ b_ind + 1], a_ind, b_ind + 1});
        }
    }
}
行分组

做法二: 行分组 (常用)
还是从上面的那个矩阵出发, 既然我们没有办法只维护 (残缺矩阵)的 左上角边缘元素
而, 边缘元素, 一定是 (每一行的 队头元素), 因此, 我们维护 (每一行的队头元素)
即, 将这矩形, 分成K个行, 每个行是递增的, 每个行单独考虑;

a(1,1) b(1,2) c(1,3) d(1,4)
e(2,1) f(2,2) g(2,3) h(2,4)
i(3,1) j(3,2) k(3,3) l(3,4)
m(4,1) n(4,2) o(4,3) q(4,4)

比如: f(2, 2)表示: f = A[2] + B[2]

最开始, 堆里维护: { {a, 1}, {e, 1}, {i, 1}, {m, 1}}, 即每一行的队头元素 与 其在该行里的(列坐标) … 下面会介绍, 不需要记录他是哪一行的
进行K次循环, 对于第a次循环, 取出heap头: {sum, col} (sum就是: 第a小数)
将该元素后面一个元素, 放入heap后
虽然我们不知道, 这个heap头元素, 他是哪个行的, 但不要紧; 比如他是row行的;
此时: sum = A[row] + B[col], 他的后一个元素是:sum_nex = A[row] + B[col + 1]
因此: sum_nex = sum - B[ col] + B[ col + 1], 因此, 不需要得到A[row]
即, 放入: {sum - B[col] + B[col + 1], col + 1}


注意点

  • 这个矩形, 不一定是K * K大小; 也可能是: LA * LB大小, 即LA < K, LB < K; 但是LA * LB >= K
    此时, 就不是划分K组, 而是LA组; 注意边界情况;

多路归并

二路归并是: 两个序列A和B, 所复合得到的C序列 的第K排序问题;

多路归并是: M个序列A, B, C, …, 所归并得到的Z序列 的第K排序问题

此时的(归并操作), 依然大致分为2类: (1, 并集) (2, 元素复合)


归并操作–并集

并集
A: a1, a2, a3, ...
B: b1, b2, b3, ...
C: c1, c2, c3, ...
D: d1, d2, d3, ...

共M个序列

此时的复合序列Z序列为: {a1, a2, ..., b1, b2, ..., c1, c2, ... ...}LA + LB + LC + LD + ...个元素;

与二路归并的做法一样, 二路归并是维护两个指针, 而这里需要维护M个指针
最开始指向 每个序列的开头;
每次: 比较这M个指针所指向的数的大小, 找到最小的;
比如tar指针指向的数, 是最小的; 将tar ++后移

归并操作–元素复合

此时, 如果还按照二路归并的做法
你得到的矩阵, 就不是二维的了, 因为Ai + Bj + Cz + ..., 而是一个M维度的矩形;
这就复杂多了, 没法将他 分成 每个行的划分;


对于一个第K小数: Ai + Bj + Cz + ..., 则其中的Ai + Bj一定在: 序列A和B的复合序列中, 是第<= K小的数

第一步: 根据A,B序列 得到一个X序列 (X序列长度为K, 存Ai + Bj的前K小数)
第二步: 根据X,C序列, 得到X序列 (X序列长度为K, 存Xi + Cj的前K小数)
第三步: 根据X,D序列, 得到X序列 (X序列长度为K, 存Xi + Dj的前K小数)

最后, X序列里, 存的就是: Ai + Bj + Cz + ...的 前K小数

例题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值