很有难度的一道题,尝试了很多次,还是没写出来
尝试
第一次尝试 二重循环 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]的状态来更新up
和down
- 当
nums[i] == nums[i+1]
的时候,更新不了任何序列 - 当
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
仍然成立 - 当
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]
的大小关系和之前上升下降的趋势来判断是否能加入,如果当前新的元素构成的趋势与之前的趋势相反,那么就能加入到摆动序列中
极值的思想就很好理解了,寻找极值即可,但是注意要去重