Leetcode376. 摆动序列——混合贪心的动态规划

文章目录

  • 前言
  • 一、题目描述
  • 二、解题思路
    • 1.概念定义
    • 2.贪心思想
    • 3.动态规划求解
  • 三、完整代码
  • 总结


前言

这是一道贪心及动态规划方面的题。做这道题的时候看官方题解的思路,我理解了好久才想通≡(▔﹏▔)≡在这里记录下我的推导思路,和官方有些区别;如果大家理解不了官方题解,可以看看这篇博文,希望有些帮助~

本文参考leetcode官方题解


一、题目描述

原题链接
Leetcode376. 摆动序列——混合贪心的动态规划_第1张图片

原序列用 n u m s nums nums表示。题目中需要注意的是一个元素就可以构成摆动序列,两个不同的元素就构成一个长度为2的摆动序列。

二、解题思路

我们使用动态规划的方法解题,但是在状态转移方程的推导过程中,需要根据贪心的思想!所以下面先介绍本题的贪心思想,再进行动态规划的思路讲解。

1.概念定义

为了方便叙述,我们先定义一些简单的小概念,大家要记住哦:

Leetcode376. 摆动序列——混合贪心的动态规划_第2张图片

2.贪心思想

我先直接写出解决本题需要的贪心思想:在原序列中,我们按照上升/下降趋势交替地选择[谷]和[峰](即…-[谷]-[峰]-[谷]-[峰]-…),这样可以使最终得到的摆动序列最长。原序列的首尾元素会依照上升/下降趋势出现在最长摆动序列中。

该思路的证明在官方题解中有讲,但是感觉不好理解。我们可以直观地想一下(可能并不严谨):

  1. 当我们处在上升趋势中时,下一个元素应该下降。如果我们选择一个尽可能小的数,之后的元素比当前选择元素大的概率就高,获得的摆动序列就可能更长;
  2. 当我们处在下降趋势中时,下一个元素应该上升。如果我们选择一个尽可能大的数,之后的元素比当前选择元素小的概率就高,获得的摆动序列就可能更长。

对于“原序列的首尾元素会依照上升/下降趋势出现在某一种摆动序列中”这句话,大家可能会有疑惑,下面来解释一下:

我们已经知道序列的首尾元素要么是[谷],要么是[峰](如果它们和相邻元素相等,那就忽略掉相邻元素,当作其不存在就好)。由于序列的首元素就是一个[谷]/[峰],所以按照贪心思想我们一定要它,之后按照上升/下降趋势交替地选择[谷]和[峰],一直遍历到尾元素;

如果尾元素是[谷],它前面一定有被选择的[峰],那么为了使摆动子序列最长,一定会把尾元素加进去,最终得到的最长摆动子序列结尾呈下降趋势;同理,若尾元素是[峰],最长的摆动子序列结尾一定呈上升趋势。

3.动态规划求解

理解了贪心算法后,我们再来看如何应用动规解题。

首先,确定状态表达式。
(大家可以先回顾一下前面定义的小概念)

  1. u p [ i ] up[i] up[i]表示以前 i i i个元素中的某一个元素为结尾的最长[上升摆动序列]的长度;
  2. d o w n [ i ] down[i] down[i]表示以前 i i i个元素中的某一个元素为结尾的最长[下降摆动序列]的长度;

下面,推导状态转移方程。

我们遍历 n u m s nums nums(即原始序列),在遍历到 n u m s [ i ] nums[i] nums[i]时,考虑 u p [ i ] , d o w n [ i ] up[i], down[i] up[i],down[i]的更新。注意当前关注的子序列是 n u m s [ : i + 1 ] nums[:i+1] nums[:i+1]

这里根据 n u m s [ i ] nums[i] nums[i]的值可以分成三种情况:
(推导预警)

  1. n u m s [ i ] > n u m s [ i − 1 ] nums[i]>nums[i-1] nums[i]>nums[i1]
    (1)若在子序列 n u m s [ : i ] nums[:i] nums[:i] n u m s [ i − 1 ] nums[i-1] nums[i1]是[峰],那么 u p [ i − 1 ] up[i-1] up[i1]对应的最长摆动序列是以 n u m s [ i − 1 ] nums[i-1] nums[i1]为结尾的。这时,
    u p [ i ] = u p [ i − 1 ] up[i]=up[i-1] up[i]=up[i1](只是把 n u m s [ : i ] nums[:i] nums[:i]中最长[上升摆动子序列] 的最后一个[峰],更新为 n u m s [ i ] nums[i] nums[i],新上升摆动序列长度不会变)
    d o w n [ i ] = d o w n [ i − 1 ] down[i]=down[i-1] down[i]=down[i1] n u m s [ i ] nums[i] nums[i] n u m s [ i − 1 ] nums[i-1] nums[i1]还大,因此不会对前面的最长[下降摆动子序列] 产生影响)
    (2)若在子序列 n u m s [ : i ] nums[:i] nums[:i] n u m s [ i − 1 ] nums[i-1] nums[i1]是[谷],那么 d o w n [ i − 1 ] down[i-1] down[i1]对应的最长摆动序列是以 n u m s [ i − 1 ] nums[i-1] nums[i1]为结尾的 。这时,
    u p [ i ] = d o w n [ i − 1 ] + 1 up[i]=down[i-1]+1 up[i]=down[i1]+1(这时可以把 n u m s [ i ] nums[i] nums[i]当作下一个[峰],加入到前面的下降摆动子序列的末尾,得到更长的新上升摆动子序列)
    d o w n [ i ] = d o w n [ i − 1 ] down[i]=down[i-1] down[i]=down[i1](原因同上)

综合前面两种小情况,可得 u p [ i ] = m a x ( d o w n [ i − 1 ] + 1 , u p [ i − 1 ] ) up[i]=max(down[i-1]+1, up[i-1]) up[i]=max(down[i1]+1,up[i1]) (二者取最大), d o w n [ i ] = d o w n [ i − 1 ] down[i]=down[i-1] down[i]=down[i1]

  1. n u m s [ i ] < n u m s [ i − 1 ] nums[i]nums[i]<nums[i1]
    (1)若在子序列 n u m s [ : i ] nums[:i] nums[:i] n u m s [ i − 1 ] nums[i-1] nums[i1]是[峰],那么 u p [ i − 1 ] up[i-1] up[i1]对应的最长摆动序列是以 n u m s [ i − 1 ] nums[i-1] nums[i1]为结尾的。这时,
    u p [ i ] = u p [ i − 1 ] up[i]=up[i-1] up[i]=up[i1] n u m s [ i ] nums[i] nums[i] n u m s [ i − 1 ] nums[i-1] nums[i1]小,因此不会对前面的最长[上升摆动子序列] 产生影响)
    d o w n [ i ] = u p [ i − 1 ] + 1 down[i]=up[i-1]+1 down[i]=up[i1]+1(这时可以把 n u m s [ i ] nums[i] nums[i]当作下一个[谷],加入到前面的上升摆动子序列的末尾,得到更长的新下降摆动子序列)
    (2)若在子序列 n u m s [ : i ] nums[:i] nums[:i] n u m s [ i − 1 ] nums[i-1] nums[i1]是[谷],那么 d o w n [ i − 1 ] down[i-1] down[i1]对应的最长摆动序列是以 n u m s [ i − 1 ] nums[i-1] nums[i1]为结尾的。这时,
    u p [ i ] = u p [ i − 1 ] up[i]=up[i-1] up[i]=up[i1](原因同上)
    d o w n [ i ] = d o w n [ i − 1 ] down[i]=down[i-1] down[i]=down[i1](只是把 n u m s [ : i ] nums[:i] nums[:i]中最长[下降摆动子序列] 的最后一个[谷],更新为 n u m s [ i ] nums[i] nums[i],新下降摆动序列长度不会变)

综合前面两种小情况,可得 u p [ i ] = u p [ i − 1 ] ) up[i]=up[i-1]) up[i]=up[i1]) , d o w n [ i ] = m a x ( d o w n [ i − 1 ] , u p [ i − 1 ] + 1 ) down[i]=max(down[i-1], up[i-1]+1) down[i]=max(down[i1],up[i1]+1)(二者取最大)

  1. n u m s [ i ] = n u m s [ i − 1 ] nums[i]=nums[i-1] nums[i]=nums[i1]
    相等的情况下,不会对前面获得的最长摆动子序列产生任何影响,因此
    u p [ i ] = u p [ i − 1 ] up[i]=up[i-1] up[i]=up[i1]
    d o w n [ i ] = d o w n [ i − 1 ] down[i]=down[i-1] down[i]=down[i1]

Leetcode376. 摆动序列——混合贪心的动态规划_第3张图片
最后在遍历完 n u m s nums nums后,我们返回 u p [ n − 1 ] , d o w n [ n − 1 ] up[n-1], down[n-1] up[n1],down[n1] n = l e n ( n u m s ) n=len(nums) n=len(nums))中的最大值作为结果。

三、完整代码

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        n = len(nums)
        if n < 2:
            return n
        
        up = [1] + [0] * (n - 1)
        down = [1] + [0] * (n - 1)
        for i in range(1, n):
            if nums[i] > nums[i - 1]:
                up[i] = max(up[i - 1], down[i - 1] + 1)
                down[i] = down[i - 1]
            elif nums[i] < nums[i - 1]:
                up[i] = up[i - 1]
                down[i] = max(up[i - 1] + 1, down[i - 1])
            else:
                up[i] = up[i - 1]
                down[i] = down[i - 1]
        
        return max(up[n - 1], down[n - 1])

由于在状态转移的过程中,当前状态只和上一个状态有关,因此我们可以进行以下简化,降低空间复杂度:

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        n = len(nums)
        if n < 2:
            return n
        
        up = down = 1
        for i in range(1, n):
            if nums[i] > nums[i - 1]:
                up = max(up, down + 1)
            elif nums[i] < nums[i - 1]:
                down = max(up + 1, down)
        
        return max(up, down)


总结

讲道理,这题好绕啊,虽然写出来的代码看上去很简单,但是想明白就需要花时间了…动态规划的状态转移方程推导着实是最难的部分,需要耐心。

每个人可能都有自己的理解方式,有时候官方题解里面的不一定适合自己理解,最好还是用自己的方式推一遍。
这个题解我写的也好不容易啊〒▽〒 希望有帮助吧~

你可能感兴趣的:(Leetcode每日一题,leetcode,动态规划,算法)