大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨🌾!
||Algorithm Day||
未来村村长正推出一系列【Algorithm Day】文章,该系列文章重在提高本人的算法能力,希望能在刷题过程中总结一般方法,提高个人的逻辑思维能力和解题能力。该系列文章以天数为轴,从一个个算法中逐步强化算法相关知识点。
”算法之路,任重而道远。“🌱|day 8|🌾
文章目录
[声明:以下题目的内容或部分解法来自牛客网或Leetcode,为个人学习总结和记录,也方便大家学习和查阅]
一、买卖股票最好时机[1]
[美的笔试第一题]
(1)描述
假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益,具体要求如下:
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费
要求:空间复杂度 O(1),时间复杂度 O(n)
(2)示例
2、思路分析
(1)思路一:动态规划
如上图所示:
- 状态确定:设dp[i][0]为第i天不持股的最大收益,dp[i][1]为第i天持股的最大收益
- 转移方程:
- dp[i][0] = MAX(dp[i-1][0],dp[i-1][1]+prices[i]):即第i天不持股的话,有两种情况,一是之前一直没持股或者之前卖掉了,那么第i天不持股的最大收益和前一天应当一致,二是当天卖掉,当天卖掉可能会产生收益,就要将前一天的收益和当天卖掉的收益进行比较【因为是顺次比较,所以只用跟前一天比】
- dp[i][1] = MAX(dp[i-1][1],-prices[i]):即第i天持股的话,也有两种情况,一是之前就持股了没有卖,那么第i天持股的最大收益和前一天应当一致,二是当天才买,则需要比较和前一天买哪一个花费更少的钱
- 初始状态:
- dp[0][0]=0:我们用0号位表示不持股的最大收益,最后返回的结果就是dp[n-1][0],这个状态决定了什么时候卖掉【发现卖掉更好就会卖】
- dp[0][1]=-prices[0]:我们用1号位表示持股的最大化收益,这个状态决定了什么时候买入【发现便宜的就会买】
- 计算顺序:从dp[0][0]和dp[0][1]到dp[n-1][0]和dp[n-1][1]
(2)思路二:双指针
我们这里定义三个变量,其中min,i为指针,maxPro为第i天的最大收益。
- 开始令min指向prices[0],然后将i向后移动。
- i每移动1次
- min就会进行一次判断,若发现i指向更小的prices[i],则更新min
- maxPro也会进行一次判断,会将prices[i]-min与当前maxPro做比较,发现更大者则更新maxPro
3、Java实现
(1)实现一:动态规划
public class Solution {
public int maxProfit (int[] prices) {
int n = prices.length;
//状态确定
int dp[][] = new int[n][2];
//初始状态
dp[0][0] = 0;//不持股
dp[0][1] = -prices[0];//持股
//计算顺序
for(int i = 1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
}
return dp[n-1][0];
}
}
(2)实现二:双指针
public class Solution {
public int maxProfit (int[] prices) {
//错误判断
if(prices==null || prices.length==0) return 0;
//指针
int min = prices[0];//min最开始指向prices[0]
int i;//用于遍历prices[]
//最大收益
maxPro = 0;
//遍历
for(i = 1;i<prices.length;i++){
min = Math.min(min,prices[i]);
maxPro = Math.max(maxPro,prices[i]-min);
}
return maxPro;
}
}
最后我们可以结合计算过程就可以很好理解,我们以[8,9,2,5,4,7,1]为例:
dp[i][0]表示的是第i天不持股的最大收益,也就是maxPro,即决定了当天是否要卖股票。d[i][1]表示的是第i天持股的最大收益,其实对应双指针中的min,即决定了当天是否要买(这里当然是买小不买大)。
二、买卖股票最好时机[2]
1、题目描述
(1)描述
假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益,具体要求如下:
-
你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
-
如果不能获取收益,请返回0
-
假设买入卖出均无手续费
(2)示例
2、思路分析
思路依旧是动态规划,和上一题的解题过程相比,只变动了状态dp[i][1]的转移方程。
我们来具体看看状态转移方程:
- dp[i][0] = MAX(dp[i-1][0],dp[i-1][1] + prices[i]):dp[i][0]依旧表示第i天不持股的最大收益,但这里的收益是累加的,从dp[i-1][1]的状态转移可以看出【决定第i天卖不卖】
- dp[i][1] = MAX(dp[i-1][1],dp[i-1][0] - prices[i]):dp[i][1]表示dii天持股的最大收益【决定第i天买不买】,如果当天买入,要判断买入后的持股收益和前一天的持股收益的大小,若买入则用不持股收益减去当日股票价格,这里的不持股收益实际上是总收益。
3、Java实现
public class Solution {
public int maxProfit (int[] prices) {
int n = prices.length;
//确定状态
int dp[][] = new int[n][2];
//初始状态
dp[0][0] = 0;
dp[0][1] = -prices[0];
//计算顺序
for(int i=1;i<n;i++){
//状态转移方程
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices[i]);
}
return dp[n-1][0];
}
}
我们继续来看计算过程,还是以[8,9,2,5,4,7,1]为例:
dp[i][1]还是决定了什么时候买,dp[i][0]决定了什么时候卖,但这里的不同是比较过程中,dp[i][1]的状态转移关联了dp[i-1][0],dp[i][0]的状态转移关联了dp[i-1][1]。
三、买卖股票的最好时机[3]
1、题目描述
(1)描述
假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益,具体要求如下:
- 你最多可以对该股票有k笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
- 如果不能获取收益,请返回0
- 假设买入卖出均无手续费
(2)示例
2、思路分析
如上图所示:
- 状态确定:我们使用dp[i][j][0]表示第i天第j笔操作后不持股的最大收益,使用dp[i][j][1]表示第i天第j笔操作后持股的最大收益
- 转移方程:
- dp[i][j][0] = MAX(dp[i-1][j][0],dp[i-1][j][1]+prices[i]):不持股依旧是两种情况,我们只需要比较当天卖出股票收益和前一天的收益大小即可。
- dp[i][j][1] = MAX(dp[i-1][j][1],dp[i-1][j-1][0] - prices[i]):持股依旧是两种情况,我们只需要比较当天买入股票收益和前一天收益大小即可。这里的j指的是交易次数,我们可以将交易次数进行独立计算,我们拥有了只交易一次的数据,就可以计算交易两次的数据。
- 初始状态:因为交易次数实际上是独立计算,但是k次交易就需要用到k-1次交易的数据,所以无论交易次数为多少,但初始状态是相同的
- 计算顺序:从第0天的交易次数为1开始,到第n-1天的交易次数为k结束
3、Java实现
class Solution {
public static int maxProfit (int[] prices, int k) {
//错误判断
if(prices==null || prices.length ==0 || k==0) return 0;
//状态确认
int n = prices.length;
int dp[][][] = new int[n][k+1][2];
//初始状态
for(int j=0;j<=k;i++){
dp[0][j][0] = 0;
dp[0][j][1] = -prices[0];
}
for(int i=1;i<n;i++){
for(int j=1;j<=k;j++){
dp[i][j][0] = Math.max(dp[i-1][j][0],dp[i-1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j-1][0] - peices[i]);
}
}
return dp[n-1][k][0];
}
}
我们结合计算过程来看,以[8,9,3,5,1,3]为例,操作次数k=2。我们可以看到,这里如果只交易一次的话,dp[n-1][1][0]=2就是最终答案,这里交易两次,这第二次交易计算dp[i][2][0]和dp[i][2][1]时,就需要用到第一次交易的数据。
四、总结
(1)什么时候用到动态规划?
计算过程是连续的、多次的、关联的,就可能用到动态规划,一般最优问题(最大值或最小值)可能用到动态规划,其次就是计数型和可行性。
(2)状态如何确定?
确定状态和转移方程是解决问题的关键,我们可以通过分析计算过程的最后一步到得出结果这个过程来推导状态和状态的转移。根据条件,我们要判断有哪几种状态,然后状态是如何转移的。如股票问题,就有以下几种状态:
- 是否持股?
- 第几天?
- 能交易几次?
我们可以通过列举的方式,把几种状态列举出来,选择具有关联性并且对结果具有影响的状态指标作为状态。
(3)状态如何转移?
状态如何转移,就是如何将原问题分解成子问题,最值的状态转移就是一个不断比较更新的过程,即是否进行值的更新,状态转移一定是一个子问题,代表我们不能受整体的影响,只需要关注前一步对当前状态的影响以及当前状态如何进行变化。