首页 > 技术文章 > 动态规划经典习题

M-Anonymous 2019-03-07 14:44 原文

 对于动态规划之类的习题,首先要确定的就是转移方程,其次是初始化数组。
 
1、篮桥杯之入学考试
问题描述:
  辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。
   医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。
   我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
  如果你是辰辰,你能完成这个任务吗?
输入格式:
  第一行有两个整数 T(1 <= T <= 1000) 和 M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。
   接下来的 M 行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式:
  包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例输入:
70 3
71 100
69 1
1 2
样例输出:
3
数据规模和约定:
  对于30%的数据,M <= 10;
  对于全部的数据,M <= 100

 辰辰从采摘第一株药草到最后一株药草的时候都面临着一个选择,摘或者不摘。

我们定义一个二维数组 dp[M][T+1] 来记录他所拥有的时间采摘药草时候的结果。

time[j]、value[j]分别表示采摘第 j 株药草的时间和价值。

假如他拥有的时间为 i,正在判断是否采摘第 j 株药草:

如果摘的话,那么他将消耗相应的时间,并采得相应的药草。dp[j][i] = dp[j-1][i-time[j]] + value[j]。

这个方程式可能不太好理解:(表示拥有 i-time[j] 的时间在采摘这株药草前所能采摘到的最大价值加上这株药草的价值)。

如果不摘的话,那么他将保持上一次采摘时候的状态。dp[j][i] = dp[j-1][i]。

那么在拥有时间 i 的情况下,采摘第 j 株药草所拥有的最大价值是 dp[j][i] =  Math.max(dp[j-1][i],dp[j-1][i-time[j]] + value[j]);

题解如下:意思是当你拥有指定时间时能采取药草的最大价值

public int solution(int[] time,int[] value,int youTime){
        int[][] dp = new int[time.length][youTime+1];
     for (int i = 1; i < time.length; i++){ for (int j = 1; j <= youTime; j++){ if (j >= time[i]){ dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-time[i]] + value[i]); }else { dp[i][j] = dp[i-1][j]; } } } return dp[time.length-1][youTime]; }

  你也可以这么写:

public int solution(int[] time,int[] value,int youTime){
        int[][] dp = new int[time.length][youTime+1];
   for (int i = 1; i <= youTime; i++){ for (int j = 1; j < time.length; j++){ if (i >= time[j]){ dp[j][i] = Math.max(dp[j-1][i],dp[j-1][i-time[j]] + value[j]); }else { dp[j][i] = dp[j-1][i]; } } } return dp[time.length-1][youTime]; }

 

2、买卖股票的最佳时机

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2
示例
2: 输入:k = 2, prices = [3,2,6,5,0,3] 输出:7 解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。 随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3
提示:
0 <= k <= 109 0 <= prices.length <= 104 0 <= prices[i] <= 1000 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

 

 买卖股票也是一样,只不过多了一些选择:

持有股票的时候只能有两个选择:继续持有,卖出;没有股票的时候也只有两个选择:不买,买。

我们定义一个三维数组 dp[day][k+1][2] 来记录交易结果。

在第 i 天的前一天时候,已经交易了 j 次(这里定义每次卖操作视为一次交易),如果手上股票:

1、选择不卖的话,那么交易次数不变,手上仍持有昨天的股票。

      今天的利润和昨天相同。dp[i][j][1] = dp[i-1][j][1]。

2、选择卖的话,那么交易次数加一,手上不再持有股票。

      今天的利润为昨天的利润加上今天股票的价格。dp[i][j+1][0] = dp[i-1][j][1] + prices[i]。

 

在第 i 天的前一天时候,已经交易了 j 次(每次卖操作视为交易一次),如果手上没有股票: 

1、选择不买的话,交易次数不会变,那么手上没有股票。

      今天的利润和昨天相同。dp[i][j][0] = dp[i-1][j][0]。

2、选择买的话,交易次数不会变,那么手上持有股票。

      今天的利润为昨天的利润减去今天股票的价格。dp[i][j][1] = dp[i-1][j][0] - prices[i]。

 

综上分析,转换方程为:

//每次卖操作视为一次交易
dp[i][j+1][0] = Math.max(dp[i-1][j+1][0],dp[i-1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j][0] - prices[i]);
//每次买操作视为一次交易
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
dp[i][j+1][1] = max(dp[i-1][j+1][1], dp[i-1][j][0] - prices[i])

交易 k 次的最大利润为 dp[prices.length-1][k][0]。

题解如下:意思是当你拥有指定交易次数时能获取的最大利润(当天数过多的情况下,会超出内存限制)

public int maxProfit(int k, int[] prices) {
        if (k == 0 || prices.length < 2){
            return 0;
        }
        int[][][] dp = new int[prices.length][k+1][2];
        for (int i = 0; i <= k; ++i){
            dp[0][i][1] = -prices[0];
        }
        for(int i = 0; i < k; i++){
            for(int j = 1; j < prices.length; j++){
                dp[j][i+1][0] = Math.max(dp[j-1][i+1][0],dp[j-1][i][1] + prices[j]);
                dp[j][i][1] = Math.max(dp[j-1][i][1],dp[j-1][i][0] - prices[j]);
            }
        }
        return dp[prices.length-1][k][0];
    }

你也可以这么写:

public int maxProfit(int k, int[] prices) {
        if (k == 0 || prices.length < 2){
            return 0;
        }
        int[][][] dp = new int[prices.length][k+1][2];
        for (int i = 0; i <= k; ++i){
            dp[0][i][1] = -prices[0];
        }
        for (int i = 1; i < prices.length; i++){
            for(int j = 0; j < k; j++){
                dp[i][j+1][0] = Math.max(dp[i-1][j+1][0],dp[i-1][j][1] + prices[i]);
                dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j][0] - prices[i]);
            }
        }
        return dp[prices.length-1][k][0];
    }

 

针对以上题解,我们探讨一下能否进行空间优化。

 1、对于蓝桥杯之入学考试的转移方程如下:

dp[j][i] = Math.max(dp[j-1][i],dp[j-1][i-time[j]] + value[j]);

我们发现采摘第 i 株药草时候的最大价值只和采摘第 i-1 株药草相关,所以应该可以简化为一维数组。

dp[i] = Math.max(dp[i],dp[i-time[j]] + value[j];

如果我们按照外层循环是时间,内层循环是药草,时间正序遍历的话:

会发现辰辰在后面的时间会采摘多株前面相同的药草,这是不可行的。

那么如果时间倒序遍历的话,dp[i-time[j]] 是取不到值的,也是不可行的

 

如果我们按照外层循环是药草,内层循环是时间,时间正序遍历的话:

仍然会发现辰辰在后面的时间会反复采摘相同的药草,这也是不可行的。

那么如果时间倒序遍历的话,后面的时间不会采摘相同的药草,dp[i-time[j]] 也是可以取到值的。

题解如下:

public int solution(int[] time,int[] value,int youTime){
        int[] dp = new int[youTime+1];
        for (int i = 0; i < time.length; i++){
            for (int j = youTime; j >= time[i]; j--){
                dp[j] = Math.max(dp[j],dp[j-time[i]] + value[i]);
            }
        }
        return dp[youTime];
    }

 

2、对于买卖股票的最佳时机的转移方程如下:

//每次卖操作视为一次交易
dp[i][j+1][0] = Math.max(dp[i-1][j+1][0],dp[i-1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j][0] - prices[i]);
//每次买操作视为一次交易
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
dp[i][j+1][1] = max(dp[i-1][j+1][1], dp[i-1][j][0] - prices[i])

我们同样可以发现在第 j 天买入、卖出、观望的最大利润也只和昨天相关,所以应该可以简化为二维数据。

//每次卖操作视为一次交易
dp[i+1][0] = Math.max(dp[i+1][0],dp[i][1] + prices[j]);
dp[i][1] = Math.max(dp[i][1],dp[i][0] - prices[j]);
//每次买操作视为一次交易
dp[i][0] = Math.max(dp[i][0],dp[i][1] + prices[j]);
dp[i+1][1] = Math.max(dp[i+1][1],dp[i][0] - prices[j]);

简化数组后会同样面临上述问题:

如果外层循环是交易次数,内层循环是天数,交易次数正序遍历的话:

我们可能会在第 n 天的时候,买入或者卖出 1 ~ n 天前的股票。

那么交易次数倒序遍历的话,那么前一次的交易记录就找不到。

如果外层循环是天数,内层循环是交易次数,交易次数倒序遍历的话:

题解如下:

public int maxProfit(int k, int[] prices) {
        if (k == 0 || prices.length < 2){
            return 0;
        }
        if (k >= prices.length/2){
            int profit = 0;
            for (int i = 0; i < prices.length-1; i++){
                if (prices[i+1] - prices[i] > 0){
                    profit += prices[i+1] - prices[i];
                }
            }
            return profit;
        }
        int[][] dp = new int[k+1][2];
        for (int i = 0; i <= k; i++){
            dp[i][1] = -prices[0];
        }
        for (int i = 1; i < prices.length; i++){
            for(int j = k-1; j >= 0; j--){
                dp[j+1][0] = Math.max(dp[j+1][0],dp[j][1] + prices[i]);
                dp[j][1] = Math.max(dp[j][1],dp[j][0] - prices[i]);
            }
        }
        return dp[k][0];
    }

 如果交易次数正序遍历也是可以的:

for (int i = 1; i < prices.length; i++){
            for(int j = 0; j < k; j++){
                dp[j+1][0] = Math.max(dp[j+1][0],dp[j][1] + prices[i]);
                dp[j][1] = Math.max(dp[j][1],dp[j][0] - prices[i]);
            }
        }

不太明白为啥正序也可以:留个链接以后看看

推荐阅读