题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
解题思路
方法1
start代表买入点,初始值为0,写出递推式:maxProfit2(prices, start)= prices[buy] - prices[sell] + maxProfit2(prices, sell+1)
/**
* 表示start处售出的最大收益
*/
public static int maxProfit2(int[] prices, int start) {
int res = 0;
if (start >= prices.length) {
return 0;
}
if(prices.length==0){
return 0;
}
// 引入记忆搜索。提高效率
if (mem[start] != 0) {
return mem[start];
}
for (int buy = 0; buy < prices.length; buy++) {
for (int sell = buy + 1; sell < prices.length; sell++) {
res = Math.max(res, prices[sell] - prices[buy] + maxProfit2(prices, sell + 1));
}
}
mem[start] = res;
return res;
}
方法2
如果反过来想,固定卖出时间 sell,向前穷举买入时间 buy,寻找 prices[buy] 最小的那天,是不是也能达到相同的效果?是的,而且这种思路可以减少一个 for 循环。
public static int maxProfit3(int[] prices, int start) {
int res = 0;
if (start >= prices.length) {
return 0;
}
if (mem[start] != 0) {
return mem[start];
}
int minPrice = prices[start];
for (int sell = start; sell < prices.length; sell++) {
minPrice = Math.min(minPrice, prices[sell]);
res = Math.max(res, maxProfit3(prices, sell + 1) + prices[sell] - minPrice);
}
mem[start] = res;
return res;
}
上面两种方法提交到LeecCode上都超时了,所以下面重点介绍状态机的解法
方法3
考虑到「不能同时参与多笔交易」,因此每天交易结束后只可能存在手里有一支股票或者没有股票的状态。
定义状态dp[i][0] 表示第 i 天交易完后手里没有股票的最大利润,dp[i][1] 表示第 i 天交易完后手里持有一支股票的最大利润(i 从 0 开始)。
考虑 dp[i][0] 的转移方程,如果这一天交易完后手里没有股票,那么可能的转移状态为前一天已经没有股票,即dp[i−1][0],或者前一天结束的时候手里持有一支股票,即dp[i−1][1],这时候我们要将其卖出,并获得 prices[i] 的收益。因此为了收益最大化,我们列出如下的转移方程:
dp[i][0]=max{dp[i−1][0],dp[i−1][1]+prices[i]}
再来考虑 dp[i][1],按照同样的方式考虑转移状态,那么可能的转移状态为前一天已经持有一支股票,即dp[i−1][1],或者前一天结束时还没有股票,即dp[i−1][0],这时候我们要将其买入,并减少prices[i] 的收益。可以列出如下的转移方程:
dp[i][1]=max{dp[i−1][1],dp[i−1][0]−prices[i]}
对于初始状态,根据状态定义我们可以知道第 0 天交易结束的时候
dp[0][0]=0, dp[0][1]=−prices[0]。
因此,我们只要从前往后依次计算状态即可。由于全部交易结束后,持有股票的收益一定低于不持有股票的收益,因此这时候dp[n−1][0] 的收益必然是大于 dp[n−1][1] 的,最后的答案即为 dp[n−1][0]。
public int maxProfit(int[] prices) {
int n = prices.length;
int dp[][] = new int[n][2];
for (int i = 0; i < n; i++) {
if (i - 1 == -1) {
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
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];
}
方法4
方法4是对方法3的优化变形
public static int maxProfit2(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = -Integer.MAX_VALUE;
// 且注意一下状态转移方程,新状态只和相邻的一个状态有关,
// 其实不用整个 dp 数组,只需要两个变量储存所需的状态就足够了,这样可以把空间复杂度降到 O(1):
for (int i = 0; i < n; i++) {
int tmp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, tmp - prices[i]);
}
return dp_i_0;
}
方法5:
贪心算法特点:一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的。
这道题,我们知道如果后面的数,比前面的数大,那么就可以进行买卖,此时利润profit>0的,可以累加。
class Solution {
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++) {
int tmp = prices[i] - prices[i - 1];
if (tmp > 0) profit += tmp;
}
return profit;
}
}
参考文章
相关题目
class Solution {
// 为了处理最多两次交易,我们需要为每天维护四种状态:
// dp[i][0]: 第 i 天结束时,没有进行过任何交易的最大利润(实际上始终为0)。
// dp[i][1]: 第 i 天结束时,进行了一次买入操作的最大利润。
// dp[i][2]: 第 i 天结束时,进行了一次买入和一次卖出的最大利润。
// dp[i][3]: 第 i 天结束时,进行了两次买入和一次卖出的最大利润。
// dp[i][4]: 第 i 天结束时,完成两次买卖的最大利润。
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) return 0;
int[][] dp = new int[n][5];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i-1][0];
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);
dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] - prices[i]);
dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i]);
}
return Math.max(Math.max(dp[n-1][0], dp[n-1][2]), dp[n-1][4]);
}
}
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
// 如果股票价格数据少于两天或没有交易次数,则无法进行任何交易
if (n <= 1 || k == 0) {
return 0;
}
// dp[i][j] 表示在第 i 天完成最多 j 笔交易的最大利润
int[][] dp = new int[n][k+1];
// 遍历每一天,更新每一天的交易情况
for (int i = 1; i < n; i++) {
// 从第 1 次交易到第 k 次交易
for (int j = 1; j <= k; j++) {
int maxProfit = 0;
// 检查在第 i 天卖出股票时,从之前的某一天买入的最大利润是多少
for (int x = 0; x < i; x++) {
// 计算如果在第 x 天买入并在第 i 天卖出的收益,并加上在第 x 天完成 j-1 笔交易的收益
maxProfit = Math.max(maxProfit, dp[x][j-1] + prices[i] - prices[x]);
}
// dp[i][j] 可以是从前一天继承而来,即没有在第 i 天进行交易,也可以是在第 i 天卖出股票后的最大利润
dp[i][j] = Math.max(dp[i-1][j], maxProfit);
}
}
// 返回在最后一天完成最多 k 笔交易的最大利润
return dp[n-1][k];
}
}