leetcode常规算法题复盘(第十二期)——摆动序列&买卖股票的最佳时机含手续费

题目原文

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

例如, [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

进阶:
你能否用 O(n) 时间复杂度完成此题?

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

  • 0 < prices.length <= 50000.
  • 0 < prices[i] < 50000.
  • 0 <= fee < 50000.

尝试解答

在博主意识到python让自己变得懒惰了之后决定用回java(项目开发还是得用python,只是java被搁置太久了得捡回来)。

两道题在逻辑上并没有复杂的地方,博主第十二期同时分享这两道题是为了能系统谈一下对DP(动态规划)的理解和认识(才不是因为懒o(*////▽////*)o)

摆动序列

 1 class Solution:
 2     def wiggleMaxLength(self, nums):
 3         if len(nums)<2:
 4             return len(nums)
 5         down_num = 1
 6         up_nums = 1
 7         down = nums[0]
 8         up = nums[0]
 9         i = 1
10         while i<len(nums):
11             print(nums)
12             print(up_nums)
13             print(up)
14             print(down_num)
15             print(down)
16             if nums[i]>nums[i-1]:
17                 up = nums[i]
18                 up_nums = down_num+1
19             elif nums[i]<nums[i-1]:
20                 down = nums[i]
21                 down_num = up_nums+1
22             i += 1
23         return max(up_nums,down_num)

买卖股票的最佳时机含手续费

 1 public class Solution {
 2     public int maxProfit(int [] prices,int fee) {
 3         int n = prices.length;
 4         int [][] dp = new int [n][2];
 5         dp[0][0] = 0;
 6         dp[0][1] = -prices[0];
 7         for(int i=1;i<n;i++) {
 8             dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]-fee);
 9             dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
10         }
11         return dp[n-1][0];
12     }
13 }

标准题解

摆动序列

思路
本题大家都很容易想到用动态规划来求解,求解的过程类似最长上升子序列,不过是需要判断两个序列。大家在实现动态规划的平方复杂度时,就会考虑如何优化到线性复杂度。

假设 up[i] 表示 nums[0:i] 中最后两个数字递增的最长摆动序列长度,down[i] 表示 nums[0:i] 中最后两个数字递减的最长摆动序列长度,只有一个数字时默认为 1。

接下来我们进行分类讨论:

nums[i+1] > nums[i]
假设 down[i] 表示的最长摆动序列的最远末尾元素下标正好为 i,遇到新的上升元素后,up[i+1] = down[i] + 1 ,这是因为 up 一定从 down 中产生(初始除外),并且 down[i] 此时最大。
假设 down[i] 表示的最长摆动序列的最远末尾元素下标小于 i,设为 j,那么 nums[j:i] 一定是递增的,因为若完全递减,最远元素下标等于 i,若波动,那么 down[i] > down[j]。由于 nums[j:i] 递增,down[j:i] 一直等于 down[j] ,依然满足 up[i+1] = down[i] + 1 。
nums[i+1] < nums[i],类似第一种情况
nums[i+1] == nums[i],新的元素不能用于任何序列,保持不变
演示
nums=[1,7,4,9,2,5] 时,演示如下:

leetcode常规算法题复盘(第十二期)——摆动序列&买卖股票的最佳时机含手续费

怎么样,是不是清晰易懂呀~

注意到 down 和 up 只和前一个状态有关,所以我们可以优化存储,分别用一个变量即可。

代码

 1 public int wiggleMaxLength(int[] nums) {
 2     int down = 1, up = 1;
 3     for (int i = 1; i < nums.length; i++) {
 4         if (nums[i] > nums[i - 1])
 5             up = down + 1;
 6         else if (nums[i] < nums[i - 1])
 7             down = up + 1;
 8     }
 9     return nums.length == 0 ? 0 : Math.max(down, up);
10 }
11 
12 //作者:lgh18
13 //链接:https://leetcode-cn.com/problems/wiggle-subsequence/solution/tan-xin-si-lu-qing-xi-er-zheng-que-de-ti-jie-by-lg/
14 //来源:力扣(LeetCode)
15 //著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

买卖股票的最佳时机含手续费

题目描述
给定每日股票价格的数组,每天可以选择是否买入/卖出,持有时不能再次买入,每笔交易有固定的手续费,求可获得的最大利润。

思路解析
这是一道入门的动态规划的题目。题目与 「秒懂 122. 买卖股票的最佳时机 II」 相比,只是多了 “手续费”。

一般的动态规划题目思路三步走:

定义状态转移方程
给定转移方程初始值
写代码递推实现转移方程
1. 定义状态转移方程
定义二维数组 dp[n][2]dp[n][2]:

dp[i][0]dp[i][0] 表示第 ii 天不持有可获得的最大利润;
dp[i][1]dp[i][1] 表示第 ii 天持有可获得的最大利润(注意是第 ii 天持有,而不是第 ii 天买入)。
定义状态转移方程:

不持有:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee)dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i]−fee)

对于今天不持有,可以从两个状态转移过来:1. 昨天也不持有;2. 昨天持有,今天卖出。两者取较大值。

持有:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])dp[i][1]=max(dp[i−1][1],dp[i−1][0]−prices[i])

对于今天持有,可以从两个状态转移过来:1. 昨天也持有;2. 昨天不持有,今天买入。两者取较大值。

2. 给定转移方程初始值
对于第 00 天:

不持有: dp[0][0] = 0dp[0][0]=0
持有(即花了 price[0]price[0] 的钱买入): dp[0][1] = -prices[0]dp[0][1]=−prices[0]
3. 写代码递推实现转移方程

 1 class Solution {
 2     public int maxProfit(int[] prices, int fee) {
 3         int n = prices.length;
 4         int[][] dp = new int[n][2];
 5         dp[0][0] = 0;
 6         dp[0][1] = -prices[0];
 7         for (int i = 1; i < n; i++) {
 8             dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee); 
 9             dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
10         }
11         return dp[n - 1][0];
12     }
13 }
14 
15 //作者:sweetiee
16 //链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/solution/jian-dan-dpmiao-dong-gu-piao-mai-mai-by-tejdo/
17 //来源:力扣(LeetCode)
18 //著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

空间优化:转移的时候,dp[i]dp[i] 只会从 dp[i-1]dp[i1] 转移得来,因此第一维可以去掉:

 1 class Solution {
 2     public int maxProfit(int[] prices, int fee) {
 3         int n = prices.length;
 4         int[] dp = new int[2];
 5         dp[0] = 0;
 6         dp[1] = -prices[0];
 7         for (int i = 1; i < n; i++) {
 8             int tmp = dp[0];
 9             dp[0] = Math.max(dp[0], dp[1] + prices[i] - fee); 
10             dp[1] = Math.max(dp[1], tmp - prices[i]);
11         }
12         return dp[0];
13     }
14 }
15 
16 //作者:sweetiee
17 //链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/solution/jian-dan-dpmiao-dong-gu-piao-mai-mai-by-tejdo/
18 //来源:力扣(LeetCode)
19 //著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度:O(n)O(n),遍历一遍即可。
空间复杂度:O(n)O(n),空间优化后是 O(1)O(1)。

思路差距

两道题都是基于动态规划的思想,我在这里根据第二道题的标准题解画了一个跟第一道题作用一样的图表帮助理解:

leetcode常规算法题复盘(第十二期)——摆动序列&买卖股票的最佳时机含手续费

 

 可以看出来,两道题的思路几乎是一摸一样的,都是基于上一个(或多个)状态对当前的最适合状态进行判断(有贪心思想的味道在里面),这就是DP,在这里我们思维发散一下,同样是基于之前的某种状态对当前状态进行判断,分治思想(或者说递归思想的某类分支)与DP的本质区别到底在什么地方?分治可以解决这两道题吗,答案是可以,但是每向前推进一次,函数就会附加一层,时间复杂度会呈现爆炸式增长,遇到超长的测试用例就会导致超时或者超迭代次数,DP为什么没有产生此类问题?是因为DP通过“容器”将前一种(或多种)状态的计算结果存储了起来,下一次计算的时候并不需要再次运行前面的算法。

以上面这两道题为例,《摆动序列》储存的是上一次的“最近的波峰、波谷”,因为我最终要求的就是波峰、波谷的最大数量;《买卖股票的最佳时机含手续费》储存的是上一次的“最大利润”,因为我最终要求的就是最后的最大利润,我用数值的形式就轻松记录了之前的计算历史,仅留下我想要的东西,何乐而不为?所以说DP的另一种说法——“用空间换时间”也是十分的有道理!那么问题来了,像《八皇后》、《解数独》这种同样需要结合“上一次状态”的回溯类题目,能不能用DP的方式轻松解决呢?答案同样是可以,但是并不轻松,因为八皇后我们要得到的是八个皇后所有可能摆放的位置,解数独我们要得到的是每个格子都填满并符合规则的所有情况,状态非常的多,每个状态都用“容器”来存储的话代价会非常很高,并且还会引入查找元素带来的时间复杂度,在每一次的推进后,当前可以出现多少种状态需要实时的判断,同样会增加时间复杂度。

因此博主认为DP与分治是相互关联的,两者需要根据实际情况进行选择最优解。

技术差距

python跟巨无霸C++、java比起来,用来做leetcode的题更加轻松、方便,用不太合适的一个词来形容,python就跟“玩具”一样,当然这么说是非常过分了,充分体现出了博主的学识浅薄,python进阶的学习成本也不见得低,但还是想分享一个很有趣的见闻:海淀的某个学生家长跟另外一个家长说:“哎呦!您孩子怎么还学python啊!那是小学二三年级学的,孩子上了五年级就该学C++了!”