376. 摆动序列

很有难度的一道题,尝试了很多次,还是没写出来

尝试

第一次尝试 二重循环 WA

对所有元素,尝试构成一个符合条件的摆动序列,然后统计最大长度,复杂度 O ( n 2 ) O(n^2) O(n2)

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if not nums:
            return 0
        ans = 1
        for i in range(len(nums)):
            if ans > len(nums)-i:
                return ans
            flag = None 
            cnt = 1
            last = nums[i]
            for j in range(i+1,len(nums)):
                # 判断特殊情况,刚进来flag为空,但是第二个数不能取得和第一个数相同
                if nums[j] == last:
                    continue
                if flag == None:
                    flag = nums[j] - last
                    last = nums[j] 
                    cnt += 1
                elif flag>0:
                    # 满足条件则更新
                    if nums[j] - last < 0:
                        flag = nums[j] - last
                        last = nums[j]
                        cnt += 1
                elif flag<0:
                    # 满足条件则更新
                    if nums[j] - last > 0:
                        flag = nums[j] - last
                        last = nums[j]
                        cnt += 1
            ans = max(ans,cnt)
        return ans 

使用二重循环来构造,我们对于后续的元素只考虑选或者不选,似乎会漏掉组合情况,感觉这里应该使用回溯来构造序列
果然卡在了某个样例上
[33,53,12,64,50,41,45,21,97,35,47,92,39,0,93,55,40,46,69,42,6,95,51,68,72,9,32,84,34,64,6,2,26,98,3,43,30,60,3,68,82,9,97,19,27,98,99,4,30,96,37,9,78,43,64,4,65,30,84,90,87,64,18,50,60,1,40,32,48,50,76,100,57,29,63,53,46,57,93,98,42,80,82,9,41,55,69,84,82,79,30,79,18,97,67,23,52,38,74,15]
对于这样的结果,输出了57,expect是67

双重循环只考虑这个元素选或者不选,如果某个元素选了,可能按照这个选择发展下去不是最优,如果某个元素不选,那么发展下去也不是最优,简而言之就是一棵决策树的分支很多,二重循环剪枝了其中大部分的结点,导致可能最长路径也被剪掉了

第二次尝试 回溯 TLE

但是回溯法做这道题,时间复杂度会一下子升到2^n,由于没给输入样例长度,不确定是否会超时

果然超时了,nice

第三次尝试 DP 没分析出来

显然每个数选或者不选,依赖于之前最后的选择结果,我们用dp来存储答案,对于输入为n的nums,dp[i]存储到i为止,出现的最长摇摆序列长度,dp[0]=1,然后dp[i]的状态取决于上它之前dp选择结果。。。然后

题解

尝试若干次未果,只好看题解做题

DP

题解的DP着实复杂,第一次看到有两个状态数组的dp,一个是up一个是down
证明也不太好理解,看了一下,说实话不太懂它更新的机制,看到下面这张图才理解了一点
在这里插入图片描述
ref: https://leetcode-cn.com/problems/wiggle-subsequence/solution/tan-xin-si-lu-qing-xi-er-zheng-que-de-ti-jie-by-lg/

主要是用down和up来维护结尾是下降和结尾是上升的两个数组,然后判断up和down的长度即可
up[i]表示从[0,i)的最长上升序列
down[i]表示从[0,i)的最长下降序列

实际上up和down的更新互相依赖于对方,因为题目要求的摆动序列,形状一定是不断波动的,每当进来一个新元素,如果这个元素能够更新up,那么up一定是从down的状态更新来的,因为down记录着下降的序列,反之同理

中间用nums[i+1]和nums[i]的状态来更新updown

  1. nums[i] == nums[i+1]的时候,更新不了任何序列
  2. nums[i+1] > nums[i]的时候,可以考虑更新up,分析down的状态
    如果down[i]对应的序列里面最后一个元素刚好是i,那么毫无疑问up[i+1] = down[i] + 1
    如果down[i]对应的序列里面最后一个元素不是i,那么我们考虑这个序列中最后一个元素下标为j,j<i,那么nums[j:i]一定是递增的,(这是因为如果是递减,那么j==i,矛盾;如果是波动的,中间一定会有下降元素导致down[i]>down[j],矛盾),考虑nums[j:i]递增,那么down[j] == down[i],up[i+] = down[i] + 1仍然成立
  3. nums[i+1] < nums[i]的时候,情况相反

实际编写代码的时候我们不用真的存储down和up的元素,只需要保存down和up的长度即可

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

贪心

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        n = len(nums)
        if n < 2:
            return n
        
        prevdiff = nums[1] - nums[0]
        ret = (2 if prevdiff != 0 else 1)
        for i in range(2, n):
            diff = nums[i] - nums[i - 1]
            if (diff > 0 and prevdiff <= 0) or (diff < 0 and prevdiff >= 0):
                ret += 1
                prevdiff = diff
        
        return ret

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/wiggle-subsequence/solution/bai-dong-xu-lie-by-leetcode-solution-yh2m/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

嗯。。。。这和我第一次尝试写得双重循环很像啊。。。为什么这就是贪心了,很迷惑

噢。。原来的双重循环我判断的是last,last若干次迭代后不一定是nums[i-1],这里是直接判断nums[i-1],判断条件不同,会导致最后选择出来的元素不同,以[1,17,13,5,10,15,11,16] 为例
在这里插入图片描述
ref: https://leetcode-cn.com/problems/wiggle-subsequence/solution/dong-tai-gui-hua-tan-xin-suan-fa-1xing-d-ig8l/

这里的判断很巧妙啊,我们遍历过程中,根据当前两个元素大小 和 之前的升降趋势来判断是否添加当前元素
其实我第一次尝试的做法已经很接近了,不过我判断的是last,就很挫
改进的代码

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if len(nums) < 2:
            return len(nums) 

        ans = 1
        flag = None
        for i in range(1,len(nums)):
            if nums[i] != nums[i-1]:
                if (nums[i] > nums[i-1]) != flag:
                    flag = nums[i] > nums[i-1] 
                    ans+=1 
        return ans

如果改成判断last就不行了,因为没有体现贪心的元素,没有贪婪的加入波峰或者波谷

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if len(nums) < 2:
            return len(nums) 

        ans = 1
        flag = None
        last = nums[0]
        for i in range(1,len(nums)):
            if nums[i] != last:
                if (nums[i] > last) != flag:
                    flag = nums[i] > last 
                    ans += 1 
                    last = nums[i]
        return ans

改进二重循环

现在考虑另一种解法,我们交替选取数组中元素的极大值和极小值加入到序列中,那么这样选取构成的序列也是最长的摆动序

画图可以得知,如果我们在下降过程中选取一个点加入,那么这个点实际可以替换为下降过程的终点,也就是极小值点,在上升过程中同理

实际上,不用交替选取极值点,只需要统计个数即可,因为两个极大值点之间必然夹着一个极小值点(证明略),反过来也成立,那么我们只需要统计极值点个数即可

现在的问题就转换为给定一个序列,统计序列中极值点的个数

对于这个问题,首先注意到如果首尾元素和它相邻的元素不相等,那么它们也是极值点,我们首先判断首尾两个元素,然后依次判断[1:n]的元素是不是极值即可

而又由于我们最差的情况下一定能取到一个点(数组不为空),所以我们只判断最后一个元素是否是极值即可

但是我们不能直接对原数组操作,因为考虑如下例子
[1,3,3,2]
显然答案是[1,3,2],但是中间的两个3都不是极值,所以我们直接判断极值,会返回错误答案2,这是不正确的,所以我们要进行去重操作

下面就是代码的编写了

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        for i in range(len(nums)-1,0,-1):
            if nums[i] == nums[i-1]:
                del nums[i]

        if len(nums) < 2:
            return len(nums)

        if len(nums) == 2:
            return 1 if nums[1] == nums[0] else 2

        ans = 1

        if nums[-1] != nums[-2]:
            ans += 1
        
        for i in range(1,len(nums)-1):
            if (nums[i-1]<nums[i]>nums[i+1]) or (nums[i-1]>nums[i]<nums[i+1]):
                ans += 1
 
        return ans

在这里插入图片描述

去除相邻元素我们可以用更巧妙的写法,使用双指针来遍历数组,相关题目: 26. 删除排序数组中的重复项

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:

        ind = 0
        for i in range(1,len(nums)):
            if nums[i]!=nums[ind]:
                # 更新
                ind += 1
                nums[ind] = nums[i]

        nums = nums[:ind+1]

        if len(nums) < 2:
            return len(nums)

        if len(nums) == 2:
            return 1 if nums[1] == nums[0] else 2



        ans = 1
        # if nums[0] != nums[1]:
            # ans += 1

        if nums[-1] != nums[-2]:
            ans += 1
        
        for i in range(1,len(nums)-1):
            if (nums[i-1]<nums[i]>nums[i+1]) or (nums[i-1]>nums[i]<nums[i+1]):
                ans += 1
 
        return ans

时间复杂度O(n),但是常数项开销比贪心算法的更大

总结

确实有难度的一道题,贪心和DP都可以解题,最后一种寻找极值的方法实际上也是一种贪心(因为我们找到了贪心策略,即寻找极值)

DP的关键在于up和down记录了上升和下降序列的信息,然后我们根据nums[i+1]和nums[i]的大小关系来判断是否能更新up和down,观察更新的过程,其实就是一个不断波动的过程,交替更新up和down

贪心的关键在于根据nums[i]nums[i-1]的大小关系和之前上升下降的趋势来判断是否能加入,如果当前新的元素构成的趋势与之前的趋势相反,那么就能加入到摆动序列中

极值的思想就很好理解了,寻找极值即可,但是注意要去重

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值