LeetCode:188. Best Time to Buy and Sell Stock IV

题目是这样的:

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]的定义不太容易。
那接下来尝试根据方程写代码:

  1. 第一次尝试,内存超出限制
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只定义两行,将每行反复使用,肯定可以节省内存。

  1. 第二次尝试,解决了内存的问题,但时间超出限制
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即可。

  1. 第三次尝试,解决了时间的问题。
    这里使用了一个取巧的方法规避超出时间限制的问题。我们观察,当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%的人。说明该方法还不完美,还有改进的空间。

  1. 第四次尝试,略微提升效率
    观察代码,其实我们在计算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%的人,至此此题结束。

  1. 总结
    我认为这题最值得思考,借鉴的还是状态是如何定义的,即怎么想到将dp[k,i]定义出来的,后面的一些内存时间上的优化都只是针对这一题而言的。

值得注意的是:以前做相关dp的时候,定义状态基本上都是:以哪一个为结尾完成了什么事。然而这里定义的状态没有,它只是说已经完成了什么,并没有说在当天必须完成一件,这样转移方程好写一些。不知道那种想法好不好弄,只不过说不要被那种想法禁锢住了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值