最近事情有点多,所以中间隔了很长时间没有写leetcode,也就很长时间没有更新博客了,望谅解哈。
今天重新开始写动态规划的题,遇到了一道非常有意思的题,必须要与大家分享一下。
问题描述:
A sequence of numbers is called a wiggle sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a wiggle sequence.
For example, [1,7,4,9,2,5] is a wiggle sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast, [1,4,7,2,5] and [1,7,4,5,5] are not wiggle sequences, the first because its first two differences are positive and the second because its last difference is zero.
Given a sequence of integers, return the length of the longest subsequence that is a wiggle sequence. A subsequence is obtained by deleting some number of elements (eventually, also zero) from the original sequence, leaving the remaining elements in their original order.
Examples:
Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.
Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].
Input: [1,2,3,4,5,6,7,8,9]
Output: 2
Follow up:
Can you do it in O(n) time?
不得不说,这道题的题目非常的长,也不容易理解。我也是看了好一会儿才看明白。大致的意思就是给你一个数组,你要从这个数组中抽出一个序列,使得序列中前一个元素减后一个元素,差值是正负、正负交叉的。这就意味着,肯定要使用数组来保存前面差值的状态,所以这道题肯定是要用动态规划的方法的了。
该怎么下手呢?首先,最容易想到的,肯定就是先用两个数组来与原数组一一对应,一个用来表示如果取在原数组中对应位置的元素时,最后一个差值为正的情况下,序列的最长长度;而另一个呢,则是用来保存最后一个差值为负的情况下,序列的最长长度。然后我们遍历原数组,每次取一个元素时,与前面的数进行比较,当比前面的某个数大时,说明它与这个数的差值为正,那么就找这个数对应的负数组的值,加1,看是否比自己对应的正数组的值大(因为它与那个数的差值为正),大就更新;不断遍历至数组起点,当前数对应的正数组的值和负数组的值就是如果取这个数时,最后一个差值为正或者为负的最长序列长度。一直循环,最后返回两个数组中的最大值,就是结果。其实过程不好说,大家可以看图:
因为是嵌套循环,所以时间复杂度为O(n2)。
下面是具体代码:
public int wiggleMaxLength(int[] nums) {
//o(n2)动态规划的方法
int length = nums.length;
if(length <= 1)
return length;
int[][] dp = new int[2][length];
int max = 1;
dp[0][0] = 1;
dp[1][0] = 1;
for(int i = 1; i < length; i++) {
int posTemp = Integer.MIN_VALUE;
int negTemp = Integer.MIN_VALUE;
//遍历前面的数,根据差值的正负来更新正负数组
for(int j = 0; j < i; j++) {
if(nums[i] - nums[j] > 0) {
posTemp = Math.max(posTemp, dp[1][j]+1);
}
else if(nums[i] - nums[j] < 0) {
negTemp = Math.max(negTemp, dp[0][j]+1);
}
}
dp[0][i] = (posTemp > Integer.MIN_VALUE ? posTemp : 1);
dp[1][i] = (negTemp > Integer.MIN_VALUE ? negTemp : 1);
}
return Math.max(dp[0][length-1], dp[1][length-1]);
}
当然啦,这一种方法是很慢的方法,实际上还是可以再优化一下的。大家仔细想想。
好吧,我来说下我的第二种方法。实际上你们有没有觉得,内循环是多余的,也就是说外循环遍历时选择一个数,然后内循环再往前遍历与这个数进行比较,其实是多余的。
其实我完全不用理你当前数前面的序列到底是选了哪一些数,我只要知道,当前数往前的整个序列,最后一个差值为正时最长的序列长度和最后一个差值为负时最长的序列长度就行,大家想想是不是这个道理。例如:
当nums[i] > nums[i-1]时:
dp[+][i] = dp[-][i-1] + 1;
dp[-][i] = dp[-][i-1];
当nums[i] < nums[i-1]时:
dp[-][i] = dp[+][i] + 1;
dp[+][i] = dp[+][i-1];
感觉这才算是真正的动态规划解法。
实际上,正负数组每一步都是会更新的,并且都是依赖前面的值,为后面的值做准备,因此我们完全不需要再使用这两个数组,直接定义两个变量,实时保存每一步两个数组的最新值就好了。
因为只进行了一次外循环,因此时间复杂度为O(n)。
下面是具体的代码:
public int wiggleMaxLength(int[] nums) {
int length = nums.length;
if(length <= 1)
return length;
//这两个就是用来保存正数组和负数组的最新值
int posTemp = 1;
int negTemp = 1;
for(int i = 1; i < length; i++) {
if(nums[i] > nums[i-1]) {
posTemp = negTemp + 1;
}
if(nums[i] < nums[i-1]) {
negTemp = posTemp + 1;
}
}
return Math.max(posTemp, negTemp);
}
你以为到这里就已经结束了吗?tan 90!因为还有另一种方法,也是O(n)的时间复杂度,但与这种方法有一点不同,下面也来说一说。
我先来举个例子先,如下图:
上面这个序列的返回的结果是:6
序列是[1、17、5、10、4、11]
大家发现了规律了吗?
就拿5,10,15来说,差值分别是5,5。前面的这个5,是肯定小于后面的这两个10,15的(因为根据差值都为正,说明这三个数是递增的)。而后面的4,很明显是一定小于15的(差值为负)。很明显,无论5和4之间取了什么数,只要中间是一个递增的序列(即差值一直都是正的),那么5肯定会小于这个序列的左边界,所以5肯定会小于这个序列的右边界(因为序列是递增的),而4肯定会小于这个序列的右边界(因为与右边界的差值为负)。所以[5,······,4]这个序列中,一定取得到[5,x,4]这个序列满足差值为正负交叉,其中x取递增序列的右边界时一定成立。
同理,当序列取[15,4,3,2,11]时,15与11之间是一个递减序列,15与11一定大于递减序列的右边界,因此这里也可以取一个[15,x,11]。
将上述两个序列组合起来,就可以得到一个满足条件的序列[5,15,4,11]。其他地方也是一样的。
这个方法的核心在哪呢?核心就在取两元素间的差值,组成一个数组,然后差值连续取正或者连续取负的区间,一定会产生一个满足条件的元素。最后将所有的元素取出来,组合起来,就是一个满足条件的序列。
时间复杂度依然是O(n),因为不需要循环嵌套。下面是代码:
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2)
return nums.length;
int prevdiff = nums[1] - nums[0];
int count = prevdiff != 0 ? 2 : 1;
for (int i = 2; i < nums.length; i++) {
int diff = nums[i] - nums[i - 1];
if ((diff > 0 && prevdiff <= 0) || (diff < 0 && prevdiff >= 0)) {
count++;
prevdiff = diff;
}
}
return count;
}
以上就是我自己结合题目的solution总结出来的三种解法。这道题虽然只是一道medium的题,但是解法很灵活,希望大家也能好好总结。
谢谢大家!