fucking-algorithm icon indicating copy to clipboard operation
fucking-algorithm copied to clipboard

动态规划设计:最长递增子序列 :: labuladong的算法小抄

Open utterances-bot opened this issue 2 years ago • 36 comments

文章链接点这里:动态规划设计:最长递增子序列

评论礼仪 见这里,违者直接拉黑。

utterances-bot avatar Jul 22 '21 03:07 utterances-bot

二分杀我

xiaoye-2018 avatar Jul 22 '21 03:07 xiaoye-2018

咋得到递增子序列呢?这个二分查找最后top数组里面是每堆纸牌最上面的那张牌,牌的堆数就是最长递增子序列长度这个没错,但是好像不太好得到那个最长的递增子序列。求大佬指教。

llnancy avatar Nov 19 '21 06:11 llnancy

agree that: "正常人基本想不到这种解法"

hprof avatar Dec 05 '21 11:12 hprof

二分法没太懂,牌的堆数就是最长递增子序列长度,如果原序列是 [5,4,3,2,1],则牌堆数为1,不等于最长递增子序列长度呀?

Joycn2018 avatar Dec 15 '21 03:12 Joycn2018

回復@Joycn2018

[5,4,3,2,1] 最长递增子序列长度是1 解集合: [5], [4], [3], [2], [1] 沒有更長的递增子序列了。

最长递增子序列的定義是 A[i] < A[j] for evey i < j

不能頭尾逆著看。

brianchiang-tw avatar Dec 24 '21 16:12 brianchiang-tw

二分法这堆牌让我想起了老windows游戏,“空当接龙”。不过规则略有改变

JackShang94 avatar Jan 11 '22 10:01 JackShang94

和蜘蛛纸牌也有点像

deepbreath373 avatar Jan 17 '22 08:01 deepbreath373

使用数学归纳法的方法, 确定dp代表的含义之后, 在已知dp[:n-1]的情况下, 给出求解dp[n]的方法. 即可迭代的写出动态规划, 这边的action, 貌似并不需要着重考虑

Asber777 avatar Jan 20 '22 10:01 Asber777

for (int i = 0; i < nums.length; i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1) 那么dp[0],怎么求,这个循环好像不能进行吧,i=0,j=0,j不<i

huangpan2507 avatar Feb 28 '22 05:02 huangpan2507

东哥,不知道是我个人总有这个问题,还是共性。 就是,看您的题解,觉得讲得非常清楚,看完立即写也写得很顺。但稍微变一丢丢就又不会了,或者说在有限的时间内大脑一片空白(比如笔试面试的时候)。我觉得只有特别熟练形成肌肉记忆,才有可能在紧张的情况下发挥出平常的水平。 再有就是,您文章中,困难也就是所谓的“坑”,您提前就给我们指好了,看完自然特别顺,但有的点,自己想是想不到的。 比如这篇文章中dp数组的定义,遇到原题说不定能知道这么定义,略微变一点,就想不到定义 以XXX结尾的XXX 啥的。 再比如打家劫舍二,你文中说了分start end讨论那两种情况,就这句话说了就会做了,但关键就在于,如果我是新拿到这道题,我就是想不到您那两种分情况讨论呀。 再比如,最大子数组我会了,那也只是当时会,我笔面试前一两天还刷过,但笔面试变成了最大乘积子数组,我立马写了个模子上去(模子大差不差,细节上不对),但是怎么当时也可能是紧张,大脑一片空白,怎么改都改不对,事后看题解维护两个dp数组,又觉得好简单。但又一次笔试,一道它的变种,但其实比它更简单,我却只能想到复杂些的方法(维护二维数组),想不到这题中那种巧妙的定义方法。 是我还不够熟练吗?可我已经读完所有东哥动态规划的文章了,也做过不止一遍了,呜呜呜~。废话有点多,不知道能不能被东哥翻。

MathsGo avatar Mar 13 '22 11:03 MathsGo

@MathsGo 感谢你把问题描述地这么清楚,我觉得不用失落,这是一个共有的问题。

首先,自己以为自己会了,并不代表真的会了。所以我反复强调看完我的文章后一定要亲去力扣敲代码,哪怕写的一模一样也得自己敲一遍,而不是仅仅复制粘贴过去提交。更进一步,我在 刷题打卡挑战 里要求读者自己输出一遍解题思路,这种输入 + 输出的方式是更有效的学习方式。

另外,面试不仅仅是技术问题,涉及到很多方面的因素,比如你说的紧张导致大脑空白发挥不好,这些都需要多练习,习惯面试的氛围之后就可以更好地展示自己的实力。

最后,任何问题归根结底还是练的不到位,需要勤加练习,多动脑思考,而不是机械性的重复。祝你早日拿到心仪的 offer。

labuladong avatar Mar 14 '22 10:03 labuladong

俄罗斯套娃那道题 用DP的解法写LIS过不了 力扣给的测试样例太变态了 超出时间限制了

Alex43975 avatar Mar 23 '22 07:03 Alex43975

图一dp[2]是不是画错了,按图解应该是dp[4]

yangyj1994 avatar Mar 23 '22 13:03 yangyj1994

@StrawHat-YYJ 确实画错了,感谢指出,我改下

labuladong avatar Mar 23 '22 14:03 labuladong

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/ 这个题解的二分查找方法的理解角度也不错~

shoren0305 avatar Mar 25 '22 00:03 shoren0305

东哥,不知道是我个人总有这个问题,还是共性。 就是,看您的题解,觉得讲得常清楚,看完立即写也写得很顺。但稍微变一丢丢就又不会了,或者说在有限的时间内大脑一片空白(比如笔试面试的时候)。我觉得只有特别熟练形成肌肉记忆,才有可能在紧张的情况下发挥出平常的水平。 再有就是,您文章中,困难也就是所谓的“坑”,您提前就给我们指好了,看完自然特别顺,但有的点,自己想是想不到的。 比如这篇文章中dp数组的定义,遇到原题说不定能知道这么定义,略微变一点,就想不到定义 以XXX结尾的XXX 啥的。 再比如打家劫舍二,你文中说了分start end讨论那两种情况,就这句话说了就会做了,但关键就在于,如果我是新拿到这道题,我就是想不到您那两种分情况讨论呀。 再比如,最大子数组我会了,那也只是当时会,我笔面试前一两天还刷过,但笔面试变成了最大乘积子数组,我立马写了个模子上去(模子大差不差,细节上不对),但是怎么当时也可能是紧张,大脑一片空白,怎么改都改不对,事后看题解维护两个dp数组,又觉得好简单。但又一次笔试,一道它的变种,但其实比它更简单,我却只能想到复杂些的方法(维护二维数组),想不到这题中那种巧妙的定义方法。 是我还不够熟练吗?可我已经读完所有东哥动态规划的文章了,也做过不止一遍了,呜呜呜~。废话有点多,不知道能不能被东哥翻。

确实是这样子啊,感觉自己的脑子就是想不出来这个规律,变一个题就又被难住了,不知道是不是自己的刷的量还没够,啥时候才能看到第一个题就可以总结出状态转移的规律啊,动态规划的题型真的是太多样了。

yangyj1994 avatar Mar 26 '22 04:03 yangyj1994

@MathsGo 你好真诚哇,我也有这个问题,我觉得这个问题一定是共性的,没有这个问题的选手我都不配做ta的竞争对手😂,所以我就想我只要尽可能的打败我这一层级的人就好了,我做不出的别人(我同层次的)也做不出就好啦。

Edith-vis avatar Mar 27 '22 05:03 Edith-vis

二分法看不懂的可以看:https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation

超出时间限制 是因为vector太慢了,转成 pair 就好了。

    int maxEnvelopes(vector<vector<int>> &envelopes)
    {
        vector<pair<int,int>> es;
        for (auto val: envelopes) es.push_back(make_pair(val[0], val[1])); // 不转成 pair 就过不了

        sort(es.begin(), es.end(), [](pair<int, int> a, pair<int, int> b){
            return a.first < b.first || (a.first == b.first && a.second > b.second);});
        vector<int> dp;
        for (auto e : es)
        {
            auto iter = lower_bound(dp.begin(), dp.end(), e.second);
            if (iter == dp.end())
                dp.push_back(e.second);
            else if (e.second < *iter)
                *iter = e.second;
        }
        return dp.size();
    }

DapengJin avatar Mar 27 '22 06:03 DapengJin

我讲一下我对纸牌游戏的理解,因为没有看到证明,所以就想了一下该怎么证

下面假设一个序列分出k个堆。

首先观察到这个序列的最长递增子序列长度不会超过k,证明如下:若原序列存在一个长度为k+1的递增子序列,考虑这个子序列中的元素e[0]<e[1]< ... <e[k],当e[0]出现时,其会占据一个堆的位置,而当e[1]出现时,其能被放置到的堆必然不同于e[0]所在的堆,因为根据放置规则,e[0]所在的堆当前的牌必然小于等于e[0],而e[1]>e[0],同理可得,e[i]放置的堆必然不同于e[0], e[1], ... e[i-1]的堆,即e[0], e[1], ... e[k]所属的堆必然两两不同,故若原序列存在一个长度为k+1的递增子序列,则这个序列必然至少有k+1个堆,与题设矛盾,因此最长递增子序列长度不会超过k。

再证最长递增子序列长度不会小于k,证明思路是从k个堆中构造一个长为k的递增子序列,证明如下:考虑堆产生的过程,按堆生成的顺序给堆编号1,2,...,k,当第k个堆生成时,前k-1个堆必然已经存在,取生成第k个堆时的数作为递增子序列的第k个数,取第k个堆生成时,第k-1个堆堆顶的数为子序列的第k-1个数,需要说明的是,这个数是满足要求的,因为其值必然小于第k个数,不然根据防止规则不可能生成第k个堆,并且显然,这个数在原序列中出现在生成第k个堆的数之前。这样依次取数,即第k-2个数为第k-1个数在放置到第k-1个堆堆顶时第k-2个堆堆顶的数,第i个数为第i+1个数放置到第i+1个堆堆顶时,第i个堆堆顶的数,这些数满足第i个数小于第i+1个数,且第i个数在第i+1个数之前出现,这样,我们构造了一个长度为k的递增子序列,又根据之前部分的证明,在这个序列中不存在长度超过k的最长递增子序列,因此序列的最长递增子序列长度为k

Lipox avatar Mar 28 '22 07:03 Lipox

刚刚和同学讨论了分堆后放牌,完全理解了。从思路上说二分不是重点,分堆是重点。只需要明白以下两点:

  1. 每一张牌一定能和相邻左边堆中的某一张牌构成递增序列(发牌的过程决定的),我们称为一个链路
  2. 每一堆的牌不能在一个递增序列(链路)中同时出现

以上两点加起来可以保证,分出的堆数就是最长的递增子序列长度。第一点保证有递增链路,第二点保证链路是最长的。

并且最后一堆的牌的个数是最长递增子序列长度的最少组合数。

NamiLing avatar Mar 28 '22 13:03 NamiLing

patience_sort(耐心排序) python verisons

# 比较直观的方式
def lis(nums):
    stack = [[]]
    for v in nums:
        for s in stack:
            if not s or s[-1] >= v:
                s.append(v)
                break
        else: # 当前纸牌点数大于当前所有堆的堆顶纸牌点数,新开一堆
            stack.append([v])
    return len(stack)

# 二分查找
def lis(nums):
    n = len(nums)
    # 最坏状态是纸牌正序排列,有多少个纸牌拆多少个堆  top数组用于维护所有堆的堆顶最小点数的纸牌
    top = [0] * n
    piles = 0
    for i in range(n):
        poker = nums[i]
        # left每次都是从0开始,right就是堆的个数,每次的任务就是把当前的poker通过二分查找放进该放的堆中
        left, right = 0, piles
        while left < right:
            mid = (left + right) // 2
            if top[mid] > poker: # 当mid位置的纸牌点数≥poker时,当前的poker放入mid左边的堆
                right = mid
            elif top[mid] < poker: # 放入mid右边的堆
                left = mid + 1
            else:
                right = mid
        if left == piles: # 当左边界和堆的个数相同时,需要新开一个堆
            piles += 1
        top[left] = poker # 更新poker放入的堆顶元素
    return piles

Alwin4Zhang avatar Mar 31 '22 05:03 Alwin4Zhang

这个二分好像和作者的思路不太一样欸

int lengthOfLIS(int[] nums) {
    int[] top = new int[nums.length];
    // 牌堆数初始化为 0
    int piles = 0;
    for (int i = 0; i < nums.length; i++) {
        // 要处理的扑克牌
        int poker = nums[i];

        /***** 搜索左侧边界的二分查找 *****/
        int left = 0, right = piles;
        while (left < right) {
            int mid = (left + right) / 2;
            if (top[mid] > poker) {
                right = mid;
            } else if (top[mid] < poker) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        /*********************************/
        
        // 没找到合适的牌堆,新建一堆
        if (left == piles) piles++;
        // 把这张牌放到牌堆顶
        top[left] = poker;
    }
    // 牌堆数就是 LIS 长度
    return piles;
}

ljrrrrr avatar Apr 08 '22 12:04 ljrrrrr

“牌的堆数就是最长递增子序列的长度,证明略” 看到证明略想了半天,想不出数学正规的证明,用实际想了一下:左小右大,右边新堆之所以出现,就是因为创建新堆的值比前面的堆尾都要大,自然就能形成递增序列。

pikapikaaa avatar Apr 22 '22 14:04 pikapikaaa

在二分法中相同的数字也是放到同一堆的

cillian-bao avatar May 12 '22 12:05 cillian-bao

int lengthOfLIS(int[] nums) {
    // 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
    int[] dp = new int[nums.length];
    // base case:dp 数组全都初始化为 1
    Arrays.fill(dp, 1);
    for (int i = 0; i < nums.length; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) 
                dp[i] = Math.max(dp[i], dp[j] + 1);
        }
    }
    
    int res = 0;
    for (int i = 0; i < dp.length; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

这段可以小优化一下

int lengthOfLIS(int[] nums) {
    // 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
    int[] dp = new int[nums.length];
    // base case:dp 数组全都初始化为 1
    Arrays.fill(dp, 1);
    int res = 0;
    for (int i = 0; i < nums.length; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) 
                dp[i] = Math.max(dp[i], dp[j] + 1);
        }
        res = Math.max(res, dp[i]);
    }
    
    return res;
}

Goolloo avatar May 20 '22 19:05 Goolloo

// envelopes = [[w, h], [w, h]...]
public int maxEnvelopes(int[][] envelopes) {
    int n = envelopes.length;
    // 按宽度升序排列,如果宽度一样,则按高度降序排列
    Arrays.sort(envelopes, new Comparator<int[]>() 
    {
        public int compare(int[] a, int[] b) {
            return a[0] == b[0] ? 
                b[1] - a[1] : a[0] - b[0];
        }
    });
    // 对高度数组寻找 LIS
    int[] height = new int[n];
    for (int i = 0; i < n; i++)
        height[i] = envelopes[i][1];

    return lengthOfLIS(height);
}

int lengthOfLIS(int[] nums) {
    // 见前文
}

排序小优化一下,也省去记忆Comparator

// envelopes = [[w, h], [w, h]...]
public int maxEnvelopes(int[][] envelopes) {
    int n = envelopes.length;
    // 按宽度升序排列,如果宽度一样,则按高度降序排列
    Arrays.sort(envelopes, (a, b) -> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
    // 对高度数组寻找 LIS
    int[] height = new int[n];
    for (int i = 0; i < n; i++)
        height[i] = envelopes[i][1];

    return lengthOfLIS(height);
}

int lengthOfLIS(int[] nums) {
    // 见前文
}

Goolloo avatar May 20 '22 20:05 Goolloo

俄罗斯套娃,LIS的解法已经不行了,会超时

bluexg7 avatar May 22 '22 00:05 bluexg7

哦。。,LIS不难用朴素的dp,得用二分查找那个解法,俄罗斯套娃才能过

bluexg7 avatar May 22 '22 00:05 bluexg7

打卡,感谢楼主!

bert82503 avatar May 25 '22 16:05 bert82503

这个length of LIS解释比leetcode会员的给的解题思路更通俗易懂,虽然官方答案是耐心排序的简化板,用一维数组(只保留堆顶数)减少空间复杂度。耐心排序的证明和解释很难在面试中完成 尝试写一个尾部开始(自上而下)的子问题动态规划但搞不定

kosplay avatar Jun 08 '22 04:06 kosplay