309号问题

来源:9-4 状态的定义和状态转移 House Robber

湿地车手

2021-10-06

bobo老师我不知道为什么309号问题我这种思路就一直超时,
我把状态定义为【第i天买入能获得的最大利润值】
然后就开始递归

    public int maxProfit(int[] prices) {
        if(prices.length <= 1) return 0;
        if(prices.length == 2) return Math.max(0, prices[1] - prices[0]);
        int[] maxP = new int[prices.length];
        Arrays.fill(maxP, -1);
        int max = 0;
        // 分别计算从第0天买入到第n-1天买入的最大值,最终得到最大的那个结果返回
        for(int i = 0; i < prices.length && maxP[i] == -1; i++) {
            max = Math.max(max, getMaxProfit(i, prices, maxP));
        }
        return max;
    }
    private int getMaxProfit(int buyIndex, int[] prices, int[] maxP) {
	    // 先计算prices的最后一天的index
        int lastDayIndex = prices.length - 1;
        // 如果当天的index已经大于等于最后一天的index,那么就不能买入直接返回0
        if(buyIndex >= lastDayIndex) {
            return 0;
        }
        // 如果当天的最大值已经计算过,就直接返回
        if(maxP[buyIndex] != -1) return maxP[buyIndex];
        // 如果当天的最大值没有计算过,就开始计算
        int res = 0;
        // 开始遍历能够卖出的日期,从buyIndex + 1开始才能卖
        for(int sellIndex = buyIndex + 1; sellIndex <= lastDayIndex; sellIndex++) {
	        // 如果sellIndex这天卖掉了,得到当前的profit
            int profit = prices[sellIndex] - prices[buyIndex];
            // 如果profit < 0 说明卖的不合理,直接跳过后面所有
            if(profit <= 0) continue;
            // 否则就是可以卖掉了,并开始循环cooldown期的长度,决定下一次买入的index
            for(int coolDown = 2; coolDown + sellIndex <= lastDayIndex + 2; coolDown++) {
	            // 获得经过当前cooldown期再次买入能够获得的最大利润
                int curRes = profit + getMaxProfit(sellIndex + coolDown, prices, maxP);
                // 选出最大的
                res = Math.max(res, curRes);
            }
        }
        // 记忆结果并返回
        return maxP[buyIndex] = res;
    }

用一个maxP[i]的数组去记录得到的第i天买入能获得的最大利润,然后分别计算从第0天买入到第n-1天买入的最大值,最终得到最大的那个结果返回。

虽然看起来要计算的步骤很多,但是其实当

        for(int i = 0; i < prices.length && maxP[i] == -1; i++) {
            max = Math.max(max, getMaxProfit(i, prices, maxP));
        }

中的i = 0代入得到结果的时候,maxP[]大部分的值就已经都算出了,后面只要遇到maxP[i] != -1的情况时候直接就可以得到最大值,所以最后的复杂度是N + N^2?这不就是常规记忆化搜索的时间复杂度吗。。。为什么一直超时呢?

老师可以稍微花点时间看一下我的代码吗,我这个思路真的想不明白为什么会超时,我注释全写在上面了谢谢谢谢。。

写回答

1回答

liuyubobobo

2021-10-06

你的逻辑没有问题。你的代码之所以超时,就是因为对于这个问题的数据量,O(n^2) 的算法就是会超时的。这个问题有 O(n) 的算法。


你的叙述里最大的问题是,“这不就是常规记忆化搜索的时间复杂度吗”。记忆化搜索没有常规的复杂度,没有任何一条规定说记忆化搜索一定是 O(n^2) 的,也没有任何一条规定说,O(n^2) 的记忆化搜索不会超时。


其实根据你的代码,可以很容易的修改成 O(n) 的算法。状态定义稍微变一下。你的状态定义表示,buyIndex 一定会买入。我下面的代码中,startIndex 表示,从 startIndex 那一天开始可以操作(但也可以不操作。)请仔细理解这两种状态定义的不同,以及根据状态定义的不同,产生的逻辑差异。(而实际上,你的代码注释虽然很完善,却唯独没有对你对状态的定义到底是什么?这一点在做 dp 的时候,是至关重要的。) 


另外要引入的一个新的状态,是操作是“买”还是“卖”,对应我的递归函数中的 op,可以选择 BUY 和 SELL 两种(对应 0 和 1)。买完以后必须卖,卖完以后必须买。


所以,我的 getMaxProfit(op, startIndex) 表示的是,在 startIndex 这一天开始,可以做 op 动作的最大利润(注意是“可以”,不是“必须”。)

class Solution {
    
    private int BUY = 0, SELL = 1;
    
    public int maxProfit(int[] prices) {
        if(prices.length <= 1) return 0;
        if(prices.length == 2) return Math.max(0, prices[1] - prices[0]);
        
        int[][] maxP = new int[2][prices.length];
        for(int i = 0; i < 2; i ++) Arrays.fill(maxP[i], Integer.MIN_VALUE);
        
        // 在我的状态定义下,直接返回 startIndex = 0 可以 BUY 的结果就好,不需要循环
        // 想想为什么?
        return getMaxProfit(BUY, 0, prices, maxP);
    }
    
    private int getMaxProfit(int op, int startIndex, int[] prices, int[][] maxP) {
    
        // 先计算prices的最后一天的index
        int lastDayIndex = prices.length - 1;
        
        if(startIndex > lastDayIndex) return 0;
        
        // 如果是 SELL,在最后一天,只能 sell 出去
        if(startIndex == lastDayIndex)
            return op == BUY ? 0 : prices[startIndex]; 
        
        // 如果当天的最大值已经计算过,就直接返回
        if(maxP[op][startIndex] != Integer.MIN_VALUE) return maxP[op][startIndex];
        
        // 如果当天的最大值没有计算过,就开始计算
        int res = 0;
        if(op == BUY){
            // 可以在这一天买
            res = -prices[startIndex] + getMaxProfit(SELL, startIndex + 1, prices, maxP);
            // 也可以这一天不买,后续再买
            res = Math.max(res, getMaxProfit(BUY, startIndex + 1, prices, maxP));
        }
        else{
            // 可以这一天卖,但注意,因为 cool down,所以卖完以后,下一次买的最早时间是 startIndex + 2
            res = prices[startIndex] + getMaxProfit(BUY, startIndex + 2, prices, maxP);
            // 也可以这一天不卖,后续再卖 
            res = Math.max(res, getMaxProfit(SELL, startIndex + 1, prices, maxP));
        }
        
        return maxP[op][startIndex] = res;
    }
}


看看上面的逻辑你是否可以看懂?有问题可以继续追问。


继续加油!:)

0
3
湿地车手
回复
liuyubobobo
感激!
2021-10-10
共3条回复

玩转算法面试-- Leetcode真题分门别类讲解

课程配套大量BAT面试真题,高频算法题解析,强化训练

7410 学习 · 1150 问题

查看课程