Code Ganker: Best Time to Buy and Sell Stock III -- LeetCode

2014年4月8日星期二

Best Time to Buy and Sell Stock III -- LeetCode

原题链接: http://oj.leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/
这道题是Best Time to Buy and Sell Stock的扩展,现在我们最多可以进行两次交易。我们仍然使用动态规划来完成,事实上可以解决非常通用的情况,也就是最多进行k次交易的情况。
这里我们先解释最多可以进行k次交易的算法,然后最多进行两次我们只需要把k取成2即可。我们还是使用“局部最优和全局最优解法”。我们维护两种量,一个是当前到达第i天可以最多进行j次交易,最好的利润是多少(global[i][j]),另一个是当前到达第i天,最多可进行j次交易,并且最后一次交易在当天卖出的最好的利润是多少(local[i][j])。下面我们来看递推式,全局的比较简单,
global[i][j]=max(local[i][j],global[i-1][j]),
也就是去当前局部最好的,和过往全局最好的中大的那个(因为最后一次交易如果包含当前天一定在局部最好的里面,否则一定在过往全局最优的里面)。对于局部变量的维护,递推式是
local[i][j]=max(global[i-1][j-1]+max(diff,0),local[i-1][j]+diff),
也就是看两个量,第一个是全局到i-1天进行j-1次交易,然后加上今天的交易,如果今天是赚钱的话(也就是前面只要j-1次交易,最后一次交易取当前天),第二个量则是取local第i-1天j次交易,然后加上今天的差值(这里因为local[i-1][j]比如包含第i-1天卖出的交易,所以现在变成第i天卖出,并不会增加交易次数,而且这里无论diff是不是大于0都一定要加上,因为否则就不满足local[i][j]必须在最后一天卖出的条件了)。
上面的算法中对于天数需要一次扫描,而每次要对交易次数进行递推式求解,所以时间复杂度是O(n*k),如果是最多进行两次交易,那么复杂度还是O(n)。空间上只需要维护当天数据皆可以,所以是O(k),当k=2,则是O(1)。代码如下:
public int maxProfit(int[] prices) {
    if(prices==null || prices.length==0)
        return 0;
    int[] local = new int[3];
    int[] global = new int[3];
    for(int i=0;i<prices.length-1;i++)
    {
        int diff = prices[i+1]-prices[i];
        for(int j=2;j>=1;j--)
        {
            local[j] = Math.max(global[j-1]+(diff>0?diff:0), local[j]+diff);
            global[j] = Math.max(local[j],global[j]);
        }
    }
    return global[2];
}
可以看到,这里的模型是比较复杂的,主要是在递推式中,local和global是交替求解的。不过理清思路之后,代码是非常简练的,不禁感叹算法真是牛逼哈,这么个复杂生活问题几行代码就解决了。

15 条评论:

  1. in local[i][j]=max(global[i-1][j-1]+max(diff,0),local[i-1][j]+diff), what is your standard for divide local[i][j] into two possible choices?

    回复删除
    回复
    1. it seems that local[i][j]=max(global[i-1][j-1]+diff,local[i-1][j]+diff) can also pass the test.

      删除
    2. 其实diff小于0确实没必要加到结果里面~ 就当成是当天买,当天卖就可以了~

      删除
    3. 还是不明白为什么local[i][j]可以划分为global[i-1][j-1]+max(diff,0) 和 local[i-1][j]+diff 这两种情况

      删除
    4. 哦,这个是因为如果利用上次的结果也就是local[i-1][j],那么交易次数就不用增加,所以是 local[i-1][j]+diff,而如果不用上次包含最后一个的交易,那么就只能多一次交易了,所以用global[i-1][j-1]加上这次的交易max(diff,0)。

      删除
    5. 多谢楼主的回答!感觉还是有点似懂非懂的样子,想的不清楚。 另外我不明白为什么local[i][j]=max(global[i-1][j-1]+diff,local[i-1][j]+diff) 也是正确的呢,这个equation明显和楼主的不同啊, 但是又有关联。

      删除
    6. 我觉得应该global[i-1][j-1]+max(diff,0)才是正确的,可能刚好没有测到这种情况而已哈~

      删除
    7. 此评论已被作者删除。

      删除
    8. 假如local[i][j]的最后一次交易是第i-3天买入,第i天卖出。那么这个交易一定是由 local[i-1][j]+diff产生的吗?

      删除
    9. 这个当然是的,因为local的定义就是必须包含最后一天的交易~

      删除
    10. 我想清楚了,你的local[i][j]的划分标准是根据它的最后一次交易的买入时间t。
      如果t<i-1,那么local[i][j]= local[i-1][j]+diff
      如果t=i-1,那么local[i][j]=global[i-1][j-1]+diff
      如果t=i, 那么local[i][j]=global[i-1][j-1]+0

      删除
    11. 代码中的for循环,为什么j是从2到1,而不是从1到2呢?j是交易次数,按道理应该从小到大来求啊。

      删除
    12. j是从2到1,大概想明白了。因为如果j从1到2,在计算j=2之前,前一天的值已经被覆盖掉了,这样无法使用一维dp了。
      另外,对于i如果用for(int i=1;i<prices.length;i++),然后diff = prices[i]-prices[i-1];在意义上可能更符合你的推导公式。虽然i=0开始,计算实际上也是一样的。

      删除
    13. 对,这个是动态规划要省空间常用的技巧哈~ 就是看数据是要旧的还是新的,这里是要旧的,所以要从大到小~

      删除
    14. 嗯,多谢楼主的指教!

      删除