【Lintcode】602. Russian Doll Envelopes

题目地址:

https://www.lintcode.com/problem/russian-doll-envelopes/description

给定一个由长度为 2 2 2的数组组成的数组 A A A,要求找到最长链 [ ( a 0 , b 0 ) , ( a 1 , b 1 ) , ( a 2 , b 2 ) , . . . , ( a k , b k ) ] [(a_0,b_0),(a_1,b_1),(a_2,b_2),...,(a_k,b_k)] [(a0,b0),(a1,b1),(a2,b2),...,(ak,bk)]使得 a i < a i + 1 a_i<a_{i+1} ai<ai+1并且 b i < b i + 1 b_i<b_{i+1} bi<bi+1。返回这个最长链的长度。

此题的 O ( n 2 ) O(n^2) O(n2)时间的动态规划算法可以参考https://blog.csdn.net/qq_46105170/article/details/108660717。以下介绍最优解,可以达到 O ( n log ⁡ n ) O(n\log n) O(nlogn)时间复杂度。但这个算法需要用到偏序集分解定理和对应的分解算法,可以参考https://blog.csdn.net/qq_46105170/article/details/108616895

先将 A A A排序,第二关键字小者优先,如果一样大,就第一关键字大者优先(关于第一关键字为啥是大者优先,主要与反链分解算法的实现有关。可以继续看下面)。接着,对数对定义一个严格偏序, x < y ⇔ x 1 < y 1 ∧ x 2 < y 2 x<y\Leftrightarrow x_1<y_1\land x_2<y_2 x<yx1<y1x2<y2(注意,上面的排序和这个严格偏序并不统一。这里的严格偏序的定义是因为题目要求和定理使用的要求)。根据严格偏序集的分解定理,其最长链从长度等于其最小反链分解的个数。我们考虑一下这里的反链是什么意思。两个数对 x x x y y y不可比较(不妨设 x x x A A A中的下标靠前),要么是 x 2 = y 2 x_2=y_2 x2=y2,要么 x 1 ≥ y 1 x_1\ge y_1 x1y1。所以这里的反链实际上就是指的非严格下降子序列。而针对这种问题的反链分解是有一个很好的算法的,具体来说如下:
1、遇到一个数字,如果其无法接到任何一个反链后面,就新开一个反链;
2、如果其能接到后面,那就找到一个末尾数字最小的数后面;这一点可以由二分来做(可以用二分处理反链是重点,正是由于能用二分,才能加快速度)。
以上接到后面的意思是,能接到反链后面依然形成反链。而分解出来的反链的末尾数字实际上是严格单调增的(数学归纳法可以证明),所以可以用二分法更新反链的末尾数字,也就是找到大于等于新来的数的最小的数。这样做的话,会比 O ( n 2 ) O(n^2) O(n2)时间的动态规划快很多,能做到 O ( n log ⁡ n ) O(n\log n) O(nlogn)时间复杂度。开一个数组 f f f f [ i ] f[i] f[i]表示下标为 i i i的反链末尾数字,则反链的个数即为所求。
接下来我们就能看到,为什么在排序的时候,第一关键字是大者优先了。原因是这样的,如果是按照小者优先,那么在二分的时候,如果没找到比新来的数大的数,那也不能决定其要开一个新的反链,因为反链的条件是,第二关键字相等或者第一关键字前者大。如果遇到了第二关键字相等的情况,那就开了新的反链,但这事实上是不对的。反例如下: A = [ ( 2 , 1 ) , ( 4 , 1 ) , ( 5 , 1 ) ] A=[(2,1),(4,1),(5,1)] A=[(2,1),(4,1),(5,1)],进行的算法如下(下面的说明里 f f f记录的是每条反链的最后一个pair,但实际代码里不需要记录第二关键字):
1、遇到 ( 2 , 1 ) (2,1) (2,1),开一个新反链,得 f = [ ( 2 , 1 ) ] f=[(2,1)] f=[(2,1)]
2、遇到 ( 4 , 1 ) (4,1) (4,1),此时前面去二分,发现没有比 4 4 4大于等于的数,于是开一个新反链,得 f = [ ( 2 , 1 ) , ( 4 , 1 ) ] f=[(2,1),(4,1)] f=[(2,1),(4,1)]
3、遇到 ( 5 , 1 ) (5,1) (5,1),此时前面去二分,发现没有比 5 5 5大于等于的数,于是开一个新反链,得 f = [ ( 2 , 1 ) , ( 4 , 1 ) , ( 5 , 1 ) ] f=[(2,1),(4,1),(5,1)] f=[(2,1),(4,1),(5,1)]
但是,实际上 A A A自己本身就是个反链,最小反链数应该是 1 1 1,而不是 3 3 3!原因就在于,二分法是无法判断第二关键字的,所以就算新来的pair能接到前面已经开的反链里形成更大的反链,但是二分判不出来。但是,如果第一关键字下降排序,这时候就没问题了,因为即使第二关键字重复了,仍然能通过二分把新来的pair接到应该接到的反链上,并且显然单调增性质没有改变(数学归纳法可以证明)。这就是为什么排序的时候,第二关键字相等的时候,要第一关键字大者优先了。这里要注意,定理本身是没错的,算法本身也没错,错就错在二分在这里失效了(当然二分可以通过修改仍然把这题做出来,但不如排序的时候就把问题解决掉)。

归结起来,算法如下:
1、先将 A A A排序,第二关键字小者优先,如果一样大,就第一关键字大者优先;
2、开一个数组 f f f f [ i ] f[i] f[i]存的是第 i i i条反链的最后一个数字(pair的第一关键字)。这里 i i i 0 0 0开始计数;
3、开始遍历 A A A,遇到新pair,直接看第一关键字,如果在 f f f中能找到比它大于等于的数,则将 f f f中的那个数更新成这个第一关键字;否则如果 f f f中所有数字都比新来的第一关键字小,就新开一个反链,把这个数接到 f f f后面。
4、遍历完成后,返回 f f f的size即可。 f f f的size就是能开的最小反链数目,根据严格偏序集的分解定理,也等于最长链的长度。

代码如下:

import java.util.Arrays;

public class Solution {
    /**
     * @param envelopes: a number of envelopes with widths and heights
     * @return: the maximum number of envelopes
     */
    public int maxEnvelopes(int[][] envelopes) {
        // write your code here
        Arrays.sort(envelopes, (e1, e2) -> e1[1] != e2[1] ? Integer.compare(e1[1], e2[1]) : -Integer.compare(e1[0], e2[0]));
        int[] f = new int[envelopes.length];
        int idx = -1;
        for (int i = 0; i < envelopes.length; i++) {
        	// 如果f里没有反链,或者最后一条反链的最后一个数小于新来的第一关键字,
        	// 就开一条新的反链,并赋值
            if (i == 0 || f[idx] < envelopes[i][0]) {
                f[++idx] = envelopes[i][0];
            } else {
            	// 否则,在f里找最小的大于等于第一关键字的数,更新之;
            	// 由于f单调,所以可以用二分
                int pos = binarySearch(f, idx, envelopes[i][0]);
                f[pos] = envelopes[i][0];
            }
        }
        
        // idx + 1就是开的反链个数
        return idx + 1;
    }
    
    private int binarySearch(int[] f, int idx, int target) {
        int l = 0, r = idx;
        while (l < r) {
            int m = l + (r - l >> 1);
            if (f[m] >= target) {
                r = m;
            } else {
                l = m + 1;
            }
        }
        
        return l;
    }
}

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值