题目是这样的:
Say you have an array for which the ith element is the price of a
given stock on day i.Design an algorithm to find the maximum profit. You may complete at
most k transactions.Note: You may not engage in multiple transactions at the same time
(ie, you must sell the stock before you buy again).
大意就是给你一支股票在一段时间内的价格,要求你通过最多完成k笔交易,所能获得的最大收益。注意你必须在买入之前把手里的卖掉。
这个题目是我在做这个系列问题的第三题时想到来做的,那个题目是最多完成两笔交易。如果我们把这题完成了,再把k设为2,就是那题的解答了。
这题明显得用动态规划的方法来做。但是状态(即子问题)如何定义是最难的。如果能够想到状态如何定义,那么写出递推式就是水到渠成的事了。
这题我们定义dp[k,i]:到第i天完成了k笔交易,所能获得的最大收益。那么状态转移方程就是:
dp[k, i] = max(dp[k, i-1], prices[i] - prices[j] + dp[k-1, j-1]), j=[0…i-1]
该方程的解释是:如果我们没有在第i天完成交易,那么dp[k,i]就等于dp[k,i-1];如果我们在第i天完成了一笔交易,卖出了在第j天买入的股票,那么dp[k,i]就等于prices[i] - prices[j] + dp[k-1, j-1]。理解应该不难,只是怎么想到dp[k,i]的定义不太容易。
那接下来尝试根据方程写代码:
- 第一次尝试,内存超出限制
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int sz = prices.size();
k = min(k, sz/2);//交易数最多为min(k, sz/2)
vector<vector<int>> dp(k+1, vector<int>(sz+1, 0));
for (int l = 1; l <= k; ++l)
for (int n = 2; n <= sz; ++n) {//至少从第二天开始才会有收益
int max = dp[l][n-1];
for (int m = 1; m < n; ++m)
if (prices[n-1] - prices[m-1] + dp[l-1][m-1] > max)
max = prices[n-1] - prices[m-1] + dp[l-1][m-1];
dp[l][n] = max;
}
return dp.back().back();
}
};
可以肯定的是,代码运行是正确的,但是当运行数据量较大的例子时,即prices很大,并且k更大,到达100000000,此时提示说内存超出限制。其实我们这里已经做了一个小优化,这里的k我们取了min(k, sz/2),因为有效的交易数最多为min(k, sz/2),因为至少需要两天才能完成一笔有收益的交易。但是优化还不够,显然需要进一步优化。
我们观察到,在循环的过程中,只用到了dp的当前行和上一行的记录,其余的全都没有用到,那么我们可以将dp只定义两行,将每行反复使用,肯定可以节省内存。
- 第二次尝试,解决了内存的问题,但时间超出限制
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (prices.empty())
return 0;
int sz = prices.size();
k = min(k, sz/2);//交易数最多为min(k, sz/2)
vector<vector<int>> dp(2, vector<int>(sz+1, 0));
for (int l = 1; l <= k; ++l)
for (int n = 2; n <= sz; ++n) {//至少从第二天开始才会有收益
int x = l % 2;//当前行
int y = 1 - x;//另一行
int max = dp[x][n-1];
for (int m = 1; m < n; ++m)
if (prices[n-1] - prices[m-1] + dp[y][m-1] > max)
max = prices[n-1] - prices[m-1] + dp[y][m-1];
dp[x][n] = max;
}
return max(dp[0].back(), dp[1].back());
}
};
这里我们将dp只定义了两行,然后反复使用。
但是问题来了,这里解决了内存的问题,但还是会超出时间限制。还是上次的例子没有通过。那么下面就解决这个问题。
另外说一句,这里的代码可以用在该系列的第三题上了,即123. Best Time to Buy and Sell Stock III。只需将这里的k设为2即可。
- 第三次尝试,解决了时间的问题。
这里使用了一个取巧的方法规避超出时间限制的问题。我们观察,当k特别大,甚至大于prices.size()/2,即我们可以交易的次数很多,那么只要某一天的价格比前一天高,那么我们就可以在前一天买入,第二天卖出。也就是说,只要有价格差,有钱可赚,那么我们就完成一笔交易,因为我们可以交易的次数很多。那么这样就只需要遍历一次prices就行了,快了很多。
这个方法是我在讨论区看来的,觉得很厉害啊,自己为什么没有想到。。。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (prices.empty())
return 0;
int sz = prices.size();
if (k >= sz/2)
return quickSolve(prices);
k = min(k, sz/2);//交易数最多为min(k, sz/2)
vector<vector<int>> dp(2, vector<int>(sz+1, 0));
for (int l = 1; l <= k; ++l)
for (int n = 2; n <= sz; ++n) {//至少从第二天开始才会有收益
int x = l % 2;//当前行
int y = 1 - x;//另一行
int max = dp[x][n-1];
if (prices[n-1] > prices[n-2]) {//只有今天的价格高于昨天的,才有可能在今天卖出去
for (int m = 1; m < n; ++m){
int temp = prices[n-1] - prices[m-1] + dp[y][m-1];
if (temp > max)
max = temp;
}
}
dp[x][n] = max;
}
return max(dp[0].back(), dp[1].back());
}
private:
int quickSolve(const vector<int> &prices) {
int res = 0, sz = prices.size();
for (int i = 1; i < sz; ++i)
if (prices[i] > prices[i-1])
res = res + (prices[i] - prices[i-1]);
return res;
}
};
这里的quickSolve()就是对应的上述的k很大的情况。
另外还可以看到,只有当今天的价格高于昨天的,才有可能在今天发生交易,否则今天的最大收益只会等于昨天的最大收益。
这里将代码提交,发现Accepted了。同时提示代码运行时间较长,只打败26.76%的人。说明该方法还不完美,还有改进的空间。
- 第四次尝试,略微提升效率
观察代码,其实我们在计算dp的那部分,对于同一行dp,我们重复计算了许多次temp。因为同一行有相同的前缀,可以将代码改为:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
if (prices.empty())
return 0;
int sz = prices.size();
if (k >= sz/2)
return quickSolve(prices);
k = min(k, sz/2);//交易数最多为min(k, sz/2)
vector<vector<int>> dp(2, vector<int>(sz+1, 0));
for (int l = 1; l <= k; ++l) {
int Min = prices[0];
for (int n = 2; n <= sz; ++n) {//至少从第二天开始才会有收益
int x = l % 2;//当前行
int y = 1 - x;//另一行
Min = min(Min, prices[n-1]-dp[y][n-1]);
dp[x][n] = max(dp[x][n-1], prices[n-1]-Min);
}
}
return max(dp[0].back(), dp[1].back());
}
private:
int quickSolve(const vector<int> &prices) {
int res = 0, sz = prices.size();
for (int i = 1; i < sz; ++i)
if (prices[i] > prices[i-1])
res = res + (prices[i] - prices[i-1]);
return res;
}
};
将代码提交后,显示运行时间击败99.9%的人,至此此题结束。
- 总结
我认为这题最值得思考,借鉴的还是状态是如何定义的,即怎么想到将dp[k,i]定义出来的,后面的一些内存时间上的优化都只是针对这一题而言的。
值得注意的是:以前做相关dp的时候,定义状态基本上都是:以哪一个为结尾完成了什么事。然而这里定义的状态没有,它只是说已经完成了什么,并没有说在当天必须完成一件,这样转移方程好写一些。不知道那种想法好不好弄,只不过说不要被那种想法禁锢住了。