首页 > 其他 > 详细

【40讲系列13】动态规划

时间:2020-11-29 22:45:26      阅读:29      评论:0      收藏:0      [点我收藏+]

一、理论

动态规划(Dynamic Programming),即动态递推。

1. 递归 + 记忆化 -> 递推

2. 状态的定义:opt[n],dp[n],fib[n]

3. 状态转移方程:opt[n] = best_of(opt[n-1], opt[n-2], ...)

4. 最优子结构

DP vs 回溯 vs 贪心

回溯(递归)—重复计算

贪心—永远局部最优

DP—记录局部最优子结构 / 多种记录值

二、典型例题

①:爬楼梯问题(LC70、剑指08.跳台阶

思路:本题是斐波那契数列的应用。

class Solution {
    public int climbStairs(int n) {
        /*if (n <= 2) return n;
        int n1 = 1;
        int n2 = 2;
        for (int i = 3; i <= n; i++) {
            n2 = n2 + n1;
            n1 = n2 - n1;
        }
        return n2;*/

        // 写成动态规划的递推形式
        if (n <= 2) return n;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

 

☆☆☆②:三角形的最小路径和(LC120数字三角形问题(动态规划)

思路:本题是DP的经典题目。需要自底向上开始递推,首先定义DP的状态,然后推出DP方程。

优化:使用一维数组而不是二维数组,这样只使用了O(n)的额外空间。

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        // 方法1: 时间复杂度和空间复杂度 都是O(n^2)
        /*int n = triangle.size();
        // 1. 状态的定义:dp[i][j]表示从点(i,j)到底边的最小路径和
        int[][] dp = new int[n + 1][n + 1];
        // 从三角形的最后一行开始递推。
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                // 2. 状态转移方程
                dp[i][j] = Math.min(dp[i + 1][j],dp[i + 1][j + 1]) + triangle.get(i).get(j);
            }
        }
        return dp[0][0];*/

        // 由于递推中,计算dp[i][j]时,只用到了下一行的dp[i+1][j]和dp[i+1][j+1]。
        // 而不需要记录整个层的结果,因此dp数组不需要定义N行,只需要一行即可。
        // 方法2:进行空间优化,仅使用一维数组. 时间复杂度O(n^2),空间复杂度O(n)
        int n = triangle.size();
        int[] dp = new int[n + 1];
        for (int i = n - 1; i >= 0; i--) {
            for (int j = 0; j <= i; j++) {
                dp[j] = Math.min(dp[j],dp[j + 1]) + triangle.get(i).get(j);
            }
        }
        return dp[0];
    }
}

 

☆☆☆③:乘积最大子数组(LC152、剑指30.连续子数组的最大和

思路:DP。关键点在于:由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin。

视频:覃超P47

class Solution {
    public int maxProduct(int[] nums) {
        int max = Integer.MIN_VALUE;
        int imax = 1, imin = 1;
        for (int i = 0; i < nums.length; i++) {
            // 由于存在负数,那么会导致最大的变最小的,最小的变最大的。
            // 当负数出现时则imax与imin进行交换再进行下一步计算
            if (nums[i] < 0){
                int temp = imax;
                imax = imin;
                imin = temp;
            }
            imax = Math.max(imax * nums[i], nums[i]);
            imin = Math.min(imin * nums[i], nums[i]);
            max = Math.max(max,imax);
        }
        return max;
    }
}

 

☆☆☆☆☆☆④:动态规划解决买卖股票问题(LC121、LC122【40讲系列7】贪心 、LC123、LC188、LC309、LC714

题目描述:

  • LC121:一次买卖一支股票                   (暴力解法、动态规划)
  • LC122:多次买卖一支股票                   (暴力解法、贪心算法、动态规划)
  • LC123:最多两次买卖一支股票                   (动态规划)
  • LC188:最多K次买卖一支股票                     (动态规划)
  • LC309:多次买卖一支股票,并且卖出股票后无法在第二天买入股票(冷冻期1天)                   (动态规划)
  • LC714:多次买卖但需要付手续费                   (动态规划)

分析:前四个属于一类问题,LC121相当于K=1的情况;LC122相当于K为无穷的情况;LC123相当于K=2的情况;LC188属于一般情况(三维动态规划)

参考:

股票问题系列通解(转载翻译)

股票问题题解

class Solution121 {
    public int maxProfit(int[] prices) {
        // DP写法1:前i天的最大收益 = max{前i-1天的最大收益,第i天的价格-前i-1天中的最小价格}
        if (prices == null || prices.length == 0) return 0;
        int min = prices[0];
        int profit = 0;
        for (int i = 1; i < prices.length; i++) {
            profit = Math.max(profit, prices[i] - min);
            min = Math.min(min, prices[i]);
        }
        return profit;
        // DP此类问题的通用写法:
        /*if (prices == null || prices.length == 0) return 0;
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1],-prices[i]);// 注意与122题对比,只允许交易一次,因此手上的现金数就是当天的股价的相反数
        }
        return dp[len-1][0];*/
        // 对上述代码进行空间优化
        /*if (prices == null || prices.length == 0) return 0;
        int dp0 = 0, dp1 = -prices[0];
        for (int i = 1; i < prices.length; i++) {
            dp0 = Math.max(dp0,dp1+prices[i]);
            dp1 = Math.max(dp1,-prices[i]);
        }
        return dp0;*/
    }
} 
class Solution122 {
    public int maxProfit(int[] prices) {
        /**
         * 方法1:贪心(针对此问题的特殊解法,因为一天可以买卖无数次)
         * 当天卖出后,当天还可以买入。只要今天比昨天大,就卖出
         */
        int res = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i - 1]){
                res += prices[i] - prices[i-1];
            }
        }
        return res;
        /**
         * 方法2.1 : 动态规划
         * 不能同时参与多笔交易,所以每天交易结束后只有持有一支股票和没有持有两种状态。
         * dp[i][0]表示第i天交易完成后没有持股的最大利润
         * dp[i][1] 表示第i天交易完成后持股的最大利润.
         * 第一种dp[i][0],说明第i天没有持股,此时有两种可能,前一天没有持股,今天也不买。或者前一天持股,在今天卖出。
         * 第二种dp[i][1],说明第i天持有股票,此时有两种可能,前一天持股,今天保持不动。或者前一天没有持股,今天买入。
         */
//        int len = prices.length;
//        int[][] dp = new int[len][2];
//        dp[0][0] = 0;
//        dp[0][1] = -prices[0];
//        for (int i = 1; i < len; i++) {
//            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
//            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
//        }
//        return dp[len - 1][0];
        /**
         * 方法2.2:动态规划
         * 由于今天的利润只和前一天相关,我们只需要记录前一天的利润。因此可以进行空间优化。
         */
//        int len = prices.length;
//        int dp0 = 0, dp1 = -prices[0];
//        for (int i = 1; i < len; i++) {
//            int newDp0 = Math.max(dp0,dp1 + prices[i]);
//            int newDp1 = Math.max(dp1,dp0 - prices[i]);
//            dp0 = newDp0;
//            dp1 = newDp1;
//        }
//        return dp0;
    }
}
class Solution123 {
    public int maxProfit(int[] prices) {
        /**
         * 针对此类问题的通用写法:
         * 注: 规定一次交易产生是在 【买入股票】 的时候,即买入股票时交易次数加1
         */
        /*if (prices == null || prices.length == 0) return 0;
        int len = prices.length;
        // 第 2 维的 0 没有意义,1 表示交易进行了 1 次,2 表示交易进行了 2 次
        int[][][] dp = new int[len][3][2];
        dp[0][1][1] = -prices[0];
        dp[0][2][1] = Integer.MIN_VALUE; //注意不能初始化为0
        for (int i = 1; i < len; i++) {
            // 转移顺序先持股,再卖出
            dp[i][1][1] = Math.max(dp[i-1][1][1], -prices[i]);
            dp[i][1][0] = Math.max(dp[i-1][1][0], dp[i-1][1][1] + prices[i]);
            dp[i][2][1] = Math.max(dp[i-1][2][1], dp[i-1][1][0] - prices[i]);
            dp[i][2][0] = Math.max(dp[i-1][2][0], dp[i-1][2][1] + prices[i]);
        }
        return Math.max(dp[len-1][1][0],dp[len-1][2][0]);*/
        /**
         * 对上述代码进行空间优化
         * 由于今天只参考了昨天的状态,所以直接去掉第一维不会影响状态转移的正确性
         */
        /*if (prices == null || prices.length == 0) return 0;
        int[][] dp = new int[3][2];
        dp[1][1] = -prices[0];
        dp[2][1] = Integer.MIN_VALUE;
        for (int i = 1; i < prices.length; i++) {
            dp[1][1] = Math.max(dp[1][1], -prices[i]);
            dp[1][0] = Math.max(dp[1][0], dp[1][1] + prices[i]);
            dp[2][1] = Math.max(dp[2][1], dp[1][0] - prices[i]);
            dp[2][0] = Math.max(dp[2][0], dp[2][1] + prices[i]);
        }
        return dp[2][0];*/ //  收益值(dp[2][0] >= dp[1][0])

        /**
         * 针对本题的变量写法:
         * buy1:第一次买入股票可获得的最大收益
         * sell1:第一次卖出股票可获得的最大收益
         */
        if (prices == null || prices.length == 0) return 0;
        int buy1 = -prices[0], buy2 = Integer.MIN_VALUE;
        int sell1 = 0, sell2 = 0;
        for (int i = 1; i < prices.length; i++) {
            buy1 = Math.max(buy1, -prices[i]);
            sell1 = Math.max(sell1, buy1 + prices[i]);
            buy2 = Math.max(buy2, sell1 - prices[i]);
            sell2 = Math.max(sell2, buy2 + prices[i]);
        }
        return sell2;
    }
}
class Solution188 {
    public int maxProfit(int k, int[] prices) {
        /**
         * 最一般的情况,相当于122题(K为无穷) 和 123题(K=2) 的融合。
         * 如果k超过一个临界值,最大收益就不再取决于最大交易次数,而是取决于数组的长度
         * 一个有收益的交易至少需要两天(在前一天买入,在后一天卖出,前提是买入价格低于卖出价格)。
         * 如果数组长度为 n,则有收益的交易的数量最多为 n/2 ,因此如果k>=n/2,等价于k为无穷的情况。
         */
        if (prices == null || prices.length == 0) return 0;
        int len = prices.length;
        if (k >= len/2){
            return greedy(prices);
        }
        int[][] dp = new int[k+1][2];
        for (int kk = 1; kk <= k; kk++) {
            dp[kk][1] = -prices[0];
        }
        for (int i = 1; i < len; i++) {
            for (int kk = 1; kk <= k; kk++) {
                dp[kk][1] = Math.max(dp[kk][1], dp[kk-1][0] - prices[i]);
                dp[kk][0] = Math.max(dp[kk][0], dp[kk][1] + prices[i]);
            }
        }
        return dp[k][0];
    }
    // 相当于k为无穷的情况,LeetCode 122
    public int greedy(int[] prices) {
        int res = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] - prices[i-1] > 0){
                res += prices[i] - prices[i-1];
            }
        }
        return res;
        // 使用DP
        /*int[][] dp = new int[prices.length][2];
        dp[0][1] = -prices[0];
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }
        return dp[prices.length - 1][0];*/
    }
}
class Solution309 {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        int len = prices.length;
        // 定义三个状态:    注意定义的状态是第i天结束后的状态
        //      1:持股;  0:不持股且不是冷冻期;  2:冷冻期(今天卖出,不持股)
        int[][] dp = new int[len][3];
        dp[0][1] = -prices[0];
        for (int i = 1; i < len; i++) {
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
            dp[i][2] = dp[i-1][1] + prices[i];
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2]);
        }
        return Math.max(dp[len-1][0], dp[len-1][2]);

        // 空间优化
        /*int dp1 = -prices[0];
        int dp0 = 0, dp2 = 0;
        for (int i = 1; i < len; i++) {
            int newDp1 = Math.max(dp1, dp0 - prices[i]);
            int newDp2 = dp1 + prices[i];
            int newDp0 = Math.max(dp0, dp2);
            dp0 = newDp0;
            dp1 = newDp1;
            dp2 = newDp2;
        }
        return Math.max(dp0,dp2);*/
    }
}
class Solution714 {
    public int maxProfit(int[] prices, int fee) {
        // 与122题类似,只是多了手续费
        if (prices == null || prices.length == 0) return 0;
        int len = prices.length;
        int[][] dp = new int[len][2];
        dp[0][1] = -prices[0] - fee;
        for (int i = 1; i < len; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
        }
        return dp[len-1][0];
        /*int dp0 = 0, dp1 = -prices[0] - fee;
        for (int i = 1; i < len; i++) {
            int newDp0 = Math.max(dp0, dp1 + prices[i]);
            int newDp1 = Math.max(dp1, dp0 - prices[i] - fee);
            dp0 = newDp0;
            dp1 = newDp1;
        }
        return dp0;*/
    }
}

 

☆☆☆☆☆⑤:最长上升子序列(LC300)

Note:该题为不连续的情况,可以延伸到最长连续子序列长度该怎么求。  另外,除了要会求子序列的长度,还要会求出最长的子序列是什么。

方法1:DP----O(n^2)

方法2:二分-------O(nlogn)。  不是很理解二分的过程 T_T

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        /**
         * 方法1:动态规划,时间复杂度O(n^2)
         */
        // 1.状态定义: dp[i]状态表示从开始到第i个元素结尾(包含第i个元素),最长子序列的长度
        // 2.DP方程:   dp[i] = max{dp[j] + 1} ( j<i, nums[j]<nums[i] )
        /*int[] dp = new int[nums.length];
        int res = 1;
        for (int i = 0; i < nums.length; i++) {
            dp[i] = 1; // 初始化。如果0到i-1中所有的数都不比第i个数小,则单独一个数是子序列
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]){
                    // 以哪个数结尾的最大递增子序列更大,就选其作为倒数第二个数
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(res, dp[i]); // 所有dp[i]中找最大的
        }
        return res;*/
        /**
         * 方法2:二分查找,时间复杂度O(nlogn)
         *  a.直接维护最长子序列数组res[],注意该数组是有序的
         *    1)如果当前元素比结果数组的值都大,就追加在结果数组的尾部(相当于递增序列长度加1)
         *    2)否则,用当前元素覆盖掉第一个比它大的元素(比它大的元素中最小的那个)。 这样就能给后面数机会
         *  b.返回数组的长度
         *  说明:由于每个数O(n)找到其要插入的位置用的是二分查找O(logn),所以总体是O(nlogn)的。
         *        2)中的覆盖操作可能会导致最终的结果数组并不是最终的递增序列,但对长度无影响。
         *  举个例子:[10,11,12,13,1,2,3,4,5]
         *          ->[10,11,12,13]->[1,11,12,13]->[1,2,12,13] ....-> [1,2,3,4,5]
         */
        int[] res = new int[nums.length];
        int len = 0; // res当前的长度
        for (int num : nums){
            int start = 0, end = len;
            // 利用二分查找res数组索引为[0, len)之间元素,找到第一个大于num的元素。
            while (start < end){
                int mid = start + ((end - start) >> 1);
                if (res[mid] < num){
                    start = mid + 1;
                }else {
                    end = mid;
                }
            }
            res[start] = num;
            if (start == len){ // 说明区间中不存在比num大的数+
                len++;
            }
            // 调二分API
            /*int idx = Arrays.binarySearch(res,0,len,num);
            idx = idx < 0 ? -idx - 1 : idx;
            res[idx] = num;
            if (idx == len) len++;*/
        }
        return len;
    }
}

【拓展】:如何根据dp数组得到最长递增子序列是什么?------>参考左神P203

    public int[] generateLIS(int[] arr, int[] dp) {
        int len = 0;
        int index = 0;
        for (int i = 0; i < dp.length; i++) {
            if (dp[i] > len){
                len = dp[i];
                index = i;
            }
        }
        int[] lis = new int[len];
        lis[--len] = arr[index];
        for (int i = index; i >= 0; i--) {
            if (arr[i] < arr[index] && dp[i] == dp[index] - 1) {
                lis[--len] = arr[i];
                index = i;
            }
        }
        return lis;
    }

 

☆☆☆⑥:零钱兑换(LC322)

思路:转换成 “跳台阶” 问题。

class Solution {
    /**
     * 思路; 转化为跳台阶问题
     *  1. 状态的定义: dp[i] 表示到i台阶时的最小步数, 也即凑齐i需要的最少硬币数
     *  2. 状态转移方程 dp[i] = min {dp[i-coins[j]} + 1
     *      举例: [1,2,5] 11
     *          dp[i] = min{i-1, i-2, i-5} + 1
     *          dp[11] = min{dp[10],dp[9],dp[6]} + 1
     */
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        // 最多的硬币数就是全部使用面值1的硬币来换, amount+1是不可能达到的换取数量。
        // 因为硬币面额最小为整数1,所以只要有解,最小硬币数必然小于amount+1
        int max = amount + 1;
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (i >= coins[j]) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
}

 

☆☆☆⑦:计算最短编辑距离问题(LC72)

 

class Solution {
    /**
     *  1. 状态的定义:dp[i][j]:word1前i个字符 变为 word2前j个字符 所需最小操作数
     *      最后返回dp[word1.length][word2.length]
     *  2. DP方程: 分两种情况,
     *      1)如果当前字母相同,则dp[i][j] = dp[i-1][j-1]
     *      2) 否则,取 删-增-替 的最小值 + 1,
     *          即 min{dp[i-1][j],dp[i][j-1],dp[i-1][j-1]} + 1
     */
    public int minDistance(String word1, String word2) {
        int len1 = word1.length(), len2 = word2.length();
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 0; i <= len1; i++) {
            dp[i][0] = i;
        }
        for (int j = 0; j <= len2; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                // 注意dp数组有一个偏移,所以是 charAt(i-1)而不是i
                if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                }else {
                    // 分别对应 增,删,替
                    dp[i][j] = Math.min(dp[i][j-1],Math.min(dp[i-1][j],dp[i-1][j-1])) + 1;
                }
            }
        }
        return dp[len1][len2];
    }
}

 

【40讲系列13】动态规划

原文:https://www.cnblogs.com/HuangYJ/p/14031237.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!