首页 > 其他 > 详细

LeetCode——376.摆动序列

时间:2020-02-06 11:38:06      阅读:61      评论:0      收藏:0      [点我收藏+]

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。
示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
示例 3:

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/wiggle-subsequence

方法 1:暴力

我们去找所有可能摆动子序列的长度并找到它们中的最大值。为了实现这样的算法,我们需要一个回溯函数,calculate(nums, index,i sUp) ,将 nums 作为输入数组,textindex 记录的是我们从哪个位置开始找最长摆动序列, boolean 变量 isUp 记录的是现在要找的是上升元素还是下降元素。如果函数 calculate 在一个上升摆动之后被调用,我们需要用这个相同的函数找到下降的元素。如果 calculate 在一个下降元素之后被调用,那么我们需要用这个函数找到下一个上升的元素。

Java

public class Solution {
    private int calculate(int[] nums, int index, boolean isUp) {
        int maxcount = 0;
        for (int i = index + 1; i < nums.length; i++) {
            if ((isUp && nums[i] > nums[index]) || (!isUp && nums[i] < nums[index]))
                maxcount = Math.max(maxcount, 1 + calculate(nums, i, !isUp));
        }
        return maxcount;
    }

    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        return 1 + Math.max(calculate(nums, 0, true), calculate(nums, 0, false));
    }
}

复杂度分析

时间复杂度: O(n!)。 calculate() 会被调用 n! 次。
空间复杂度: O(n)。回溯深度为 n。

方法 2:动态规划

算法

为了更好地理解这一方法,用两个数组来 dp ,分别记作 up 和 down 。

每当我们选择一个元素作为摆动序列的一部分时,这个元素要么是上升的,要么是下降的,这取决于前一个元素的大小。

up[i]up[i] 存的是目前为止最长的以第 ii 个元素结尾的上升摆动序列的长度。

类似的, down[i] 记录的是目前为止最长的以第 ii 个元素结尾的下降摆动序列的长度。

我们每当找到将第 ii 个元素作为上升摆动序列的尾部的时候就更新 up[i]up[i] 。现在我们考虑如何更新 up[i]up[i] ,我们需要考虑前面所有的降序结尾摆动序列,也就是找到 down[j]down[j] ,满足 j<i 且 nums[i]>nums[j] 。类似的, down[i] 也会被更新。

java

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int[] up = new int[nums.length];
        int[] down = new int[nums.length];
        for (int i = 1; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    up[i] = Math.max(up[i],down[j] + 1);
                } else if (nums[i] < nums[j]) {
                    down[i] = Math.max(down[i],up[j] + 1);
                }
            }
        }
        return 1 + Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

复杂度分析

时间复杂度: O(n^2)。 循环内嵌套了一个循环。
空间复杂度: O(n)O(n) 。 dp 需要两个同样长度的数组。

方法 3:线性动态规划

算法

数组中的任何元素都对应下面三种可能状态中的一种:

上升的位置,意味着 nums[i]>nums[i?1]
下降的位置,意味着 nums[i]<nums[i?1]
相同的位置,意味着 nums[i]==nums[i?1]
更新的过程如下:

如果 nums[i]>nums[i?1] ,意味着这里在摆动上升,前一个数字肯定处于下降的位置。所以 up[i]=down[i?1]+1 , down[i]down[i] 与 down[i?1] 保持相同。

如果 nums[i]<nums[i?1] ,意味着这里在摆动下降,前一个数字肯定处于下降的位置。所以 down[i]=up[i?1]+1 , up[i]up[i] 与 up[i-1]up[i?1] 保持不变。

如果 nums[i]==nums[i?1] ,意味着这个元素不会改变任何东西因为它没有摆动。所以 up[i] 与 down[i-1]down[i?1] 和 up[i-1]up[i?1] 都分别保持不变。

最后,我们可以将 up[length?1] 和 down[length?1] 中的较大值作为问题的答案,其中 length 是给定数组中的元素数目。

下面的例子可以说明这一过程:

Java

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int[] up = new int[nums.length];
        int[] down = new int[nums.length];
        up[0] = down[0] = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = down[i - 1] + 1;
                down[i] = down[i - 1];
            } else if (nums[i] < nums[i - 1]) {
                down[i] = up[i - 1] + 1;
                up[i] = up[i - 1];
            } else {
                down[i] = down[i - 1];
                up[i] = up[i - 1];
            }
        }
        return Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

复杂度分析

时间复杂度: O(n)O(n) 。只需要遍历数组一遍。
空间复杂度: O(n)O(n) 。 dp需要两个相同长度的数组。

方法 4: 空间优化的动态规划

算法

这个方法与方法 3 一样。但我们观察可以发现, DP 过程中更新 up[i]up[i] 和 down[i]down[i] ,其实只需要 up[i-1]up[i?1] 和 down[i-1]down[i?1] 。因此,我们可以通过只记录最后一个元素的值而不使用数组来节省空间。

Java

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int down = 1, up = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1])
                up = down + 1;
            else if (nums[i] < nums[i - 1])
                down = up + 1;
        }
        return Math.max(down, up);
    }
}

复杂度分析

时间复杂度: O(n)O(n) 。仅遍历数组一次。
空间复杂度: O(1)O(1) 。只使用了常数级别的额外空间。

方法 5:贪心

算法

我们其实不需要 dp 来解决这个问题。这个问题等价于找数组中交替的最大最小值。因此,如果我们选择中间的数字作为摆动序列的一部分,只会导致序列小于等于只选连续的最大最小元素。

下图可以更好地说明此方法:

从上图中,我们可以看到,如果我们选择 C 而不是 D 作为摆动序列的第二个点,我们无法将点 E 也包括进最终序列。因此,我们无法得到最长摆动序列。

因此,为了解决这个问题,我们维护一个变量 prevdiff ,它的作用是只是当前数字的序列是上升还是下降的。如果 0prevdiff>0 ,那么表示目前是上升序列,我们需要找一个下降的元素。因此,我们更新已找到的序列长度 diff (nums[i]?nums[i?1])为负数。类似的,如果 0prevdiff<0 ,我们就更新 diff (nums[i]?nums[i?1])为正数。

当整个数组都被遍历后,我们得到了最终的结果,它就是最长摆动子序列的长度。

Java

public class Solution {
    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;
    }
}

复杂度分析

时间复杂度: O(n)O(n) 。我们需要遍历数组一次。

空间复杂度: O(1)O(1) 。不需要使用额外的空间。

题目中给的tag说明了这道题可以用DP和Greedy两种方法来做,那么我们先来看DP的做法,我们维护两个dp数组p和q,其中p[i]表示到i位置时首差值为正的摆动子序列的最大长度,q[i]表示到i位置时首差值为负的摆动子序列的最大长度。我们从i=1开始遍历数组,然后对于每个遍历到的数字,再从开头位置遍历到这个数字,然后比较nums[i]和nums[j],分别更新对应的位置,参见代码如下:

C++解法一:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        if (nums.empty()) return 0;
        vector<int> p(nums.size(), 1);
        vector<int> q(nums.size(), 1);
        for (int i = 1; i < nums.size(); ++i) {
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) p[i] = max(p[i], q[j] + 1);
                else if (nums[i] < nums[j]) q[i] = max(q[i], p[j] + 1);
            }
        }
        return max(p.back(), q.back());
    }
};

题目中有个Follow up说要在O(n)的时间内完成,而Greedy算法正好可以达到这个要求,这里我们不在维护两个dp数组,而是维护两个变量p和q,然后遍历数组,如果当前数字比前一个数字大,则p=q+1,如果比前一个数字小,则q=p+1,最后取p和q中的较大值跟n比较,取较小的那个,参见代码如下:

C++解法二:

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int p = 1, q = 1, n = nums.size();
        for (int i = 1; i < n; ++i) {
            if (nums[i] > nums[i - 1]) p = q + 1;
            else if (nums[i] < nums[i - 1]) q = p + 1;
        }
        return min(n, max(p, q));
    }
};

Python动态规划

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        if not nums:
            return 0
        res = []
        dp = [[1 for i in range(2)] for j in range(len(nums))]
        res.append(dp[0][0])
        res.append(dp[0][1])
        for i in range(1,len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i][1] = max(dp[i][1],dp[j][0]+1)
                elif nums[i] < nums[j]:
                    dp[i][0] = max(dp[i][0],dp[j][1]+1)
                else:               #若nums[i]=nums[j],此时nums[i]不可能加在nums[j]后面
                    continue
            res.append(dp[i][0])
            res.append(dp[i][1])
        return max(res)

#dp[i][0]表示以nums[i]结尾且当前位置为降序的最长摆动序列,dp[i][1]表示以nums[i]结尾且当前位置为升序的最长摆动序列

C++遍历

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int N = nums.size();
        if (N < 2) return N;
        int prev = 0;
        int res = 1;
        for (int i = 1; i < N; ++i) {
            if (nums[i] == nums[i - 1]) continue;
            int curr = (nums[i] > nums[i - 1]) ? 1 : -1;
            res += curr != prev;
            prev = curr;
        }
        return res;
    }
};

结论为:去掉所有处于非波峰或波谷的点结果就是最大波动点
反证法证明
如图,假设Y1左边和Y7右边已经达到最大点。假设存在Y2是非波峰使得当前的四个有效点+1变为5个。
因为Y2>Y1。
又因为 Y2<Y3
所以 选择Y2 必然丢弃Y3.
而如果Y3丢弃之后的点有一条更好的路径,那么必然是Y1< Y2 >Yxx。并且4个有效点+1变为5个。
那么Y1< Y3 >yxx显然也能够出现结果为5个点。显然与已知的结论四个点矛盾。

 public int wiggleMaxLength(int[] nums) {
        if (nums.length < 1) {
            return 0;
        }
        int lastDir = 0;
        int suc = 1;
        int start = 0;
        int lastValidValue = nums[0];
        for (start = 1; start < nums.length; start++) {
            if (nums[start] != nums[start - 1]) {
                lastDir = nums[start] > nums[start - 1] ? 1 : -1;//斜率
                lastValidValue = nums[start];
                suc=suc+1;
                break;
            }
        }


        for (; start < nums.length; start++) {
            if (-1 * lastDir * (nums[start] - nums[start - 1]) > 0) {
                suc++;
                lastDir = -1 * lastDir;
            } lastValidValue = nums[start];
        }
        return suc;
    }

LeetCode——376.摆动序列

原文:https://www.cnblogs.com/wwj99/p/12267938.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!