• 【40讲系列13】动态规划


    一、理论

    递归问题 -> 重叠子问题、最优子结构 -> 记忆化搜索(自顶向下) -> 动态规划(记忆化 + 自底向上)

    动态规划(Dynamic Programming),即动态递推(递归 + 记忆化 -> 递推)。动态规划问题的一般形式就是求最值。

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

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

    3. 存在重叠子问题,具备最优子结构. (最优子结构是指通过求子问题的最优解,可以获得原问题的最优解)

    4. 自底向上。(这也是为什么动态规划不用递归,而是要循环迭代完成计算)

    5. 状态压缩(滚动数组)。如果发现每次状态转移只需要DP数组的一部分,那么可以运用【滚动数组】的思想,进行状态压缩,只记录必要的数据,优化空间。

     

    题目特点:动态规划的题目分为两大类,

      一种是求最优解类,典型问题是背包问题。 当前问题的最优解取决于子问题的最优解。

      另一种是计数类,如统计方案数等问题。 当前问题的方案数取决于子问题的方案数。

    动态规划的解题步骤:

    • 定义子问题
    • 写出子问题的递推关系
    • 确定 DP 数组的计算顺序
    • 空间优化(可选)

    DP vs 回溯 vs 贪心

    回溯(递归)—重复计算

    贪心—永远局部最优

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

    Note:何时用【回溯】,何时用【动态规划】?  —— 如果是求走迷宫的【路径】,必然是回溯; 如果是走迷宫的【路径的条数 / 方案数】,必然是dp。

    二、从斐波那契数谈起

      经典的斐波那契数列的递归解法如下所示(时间复杂度为:O(2^n),为指数级复杂度),它的缺点在于有大量的子问题被重复计算。

    class Solution {
        public int fib(int n) {
            // 执行耗时:10 ms,击败了17.41% 的Java用户
            if (n == 0) return 0;
            if (n == 1) return 1;
            return fib(n - 1) + fib(n - 2);
        }
    }

      优化方案1:记忆化搜索,自顶向下。 (时间复杂度:O(n),仍然需要递归)

    class Solution {
        // 假设 memo[]数组已赋好初值,初始化都为-1
        public int fib(int n) {
            if (n == 0) return 0;
            if (n == 1) return 1;
            if (memo[n] == -1) { // 如果没计算过
                memo[n] = fib(n-1) + fib(n-2);
            }
            return memo[n];
        }
    }

      优化方案2:动态规划----记忆化自底向上。      (时间复杂度为O(n))

    class Solution {
        public int fib(int n) {
            //        执行耗时:0 ms,击败了100.00% 的Java用户
            //        内存消耗:35.3 MB,击败了40.50% 的Java用户
            if (n == 0 || n == 1) return n;
            int[] arr = new int[n + 1];
            arr[0] = 0;
            arr[1] = 1;
            for (int i = 2; i <= n; i++) {
                arr[i] = arr[i-1] + arr[i-2];
            }
            return arr[n];
        }
    }

      优化方案3:空间优化  (当前状态只和之前两个状态有关,所以只存储之前两个状态即可,空间复杂度可降为O(1))

    class Solution {
        public int fib(int n) {
            //        执行耗时:0 ms,击败了100.00% 的Java用户
            //        内存消耗:35.1 MB,击败了79.25% 的Java用户
            /*
            if (n == 0 || n == 1) return n;
            int n1 = 1; // 第n-1项
            int n2 = 0; // 第n-2项
            int sum = 0;
            for (int i = 2; i <= n; i++) {
                sum = n1 + n2;
                n2 = n1;
                n1 = sum;
            }
            return sum;
            */
            // 进一步优化
            if (n == 0 || n == 1) return n;
            int sum = 1;  // 利用 sum 存储第 n-1 项
            int n2 = 0;
            for (int i = 2; i <= n; i++) {
                sum = sum + n2;
                n2 = sum - n2;
            }
            return sum;
        }
    }

    三、0-1背包问题(参考资料:bobo、背包九讲)

      问题描述:背包的容量为C,有n中不同的物品,编号为0...n-1,每一件物品的重量为w(i),价值为v(i)。问可以向背包中放哪些物品,使得在不超过背包容量的基础上,物品的总价值最大。

      问题初探:该问题不能使用贪心算法(优先放入平均价值最高的物品),举例可发现行不通。

      问题分析:设F(n,c)为考虑将n个物品放在容量为c的背包中,使得价值最大。每一个物品都有 放 与 不放,两种状态。

           F(i,c) = max( F(i-1,c) , v(i) + F( i-1, c - w(i)) )

           重叠子问题在:同样的 i 和 c数据对,可能计算了多次。

      0-1背包问题的复杂度分析: 时间复杂度O(n*C)  空间复杂度O(n*C)

    0-1问题的优化1(滚动数组):由状态转移方程可以看出,第 i 行元素只依赖于第i-1行元素。理论上,只需要保持两行元素,空间复杂度可以优化为O(2*C) = O(C)。  具体实现上,可以根据 i 的奇偶性,轮流使用两行数组

    0-1问题的优化2(只使用一维数组):由于第二行当前格子的计算依赖于前一行上边左边 某个格子的信息,而不会碰右边的格子。因此,只有一行时,可以从右向左进行计算。

    0-1背包问题的变种

      (1)完全背包问题:每个物品可以无限使用。         

        分析:由于背包容量是有限的,那么每个物品可以取到的个数有最大值,因此,可以转化为普通背包问题,区别在于物品选取列表中,很多物品是重复的。更进一步,可以用二进制优化重复物品的个数,如1,2,4,8,16...。

      (2)多重背包问题:每个物品不止1个,有num(i)个。

      (3)多维费用背包问题:要考虑物品的体积和重量两个维度?

        分析:状态需要多一个参数,设置三维数组

      (4)物品间约束,如物品间相互依赖或相互排斥。

    背包问题题型归纳与技巧:希望用一种规律搞定背包问题

    四、典型例题

    ①:爬楼梯问题(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];
        }
    }

    五、扩展例题

    第一组:LeetCode120. 三角形最小路径和LeetCode64. 最小路径和

    第二组(发现重叠子问题):LeetCode343. 整数拆分LeetCode279. 完全平方数LeetCode91. 解码方法LeetCode62. 不同路径LeetCode63. 不同路径 II

    第三组(状态定义和状态转移):LeetCode198. 打家劫舍LeetCode213. 打家劫舍 IILeetCode337. 打家劫舍 IIILeetCode309. 最佳买卖股票时机含冷冻期

    第四组(0-1背包问题):LeetCode416. 分割等和子集LeetCode322. 零钱兑换LeetCode377. 组合总和 ⅣLeetCode474. 一和零LeetCode139. 单词拆分LeetCode494. 目标和

    第五组(LIS问题):LeetCode300. 最长递增子序列LeetCode376. 摆动序列

    第六组(各种子序列问题):LeetCode674. 最长连续递增序列LeetCode1143. 最长公共子序列LeetCode128. 最长连续序列LeetCode392. 判断子序列

  • 相关阅读:
    设计模式学习之工厂方法(Factory Method,创建型模式)(2)
    设计模式学习之简单工厂(Simple Factory,创建型模式)(1)
    JAVA基础学习之 Map集合、集合框架工具类Collections,Arrays、可变参数、List和Set集合框架什么时候使用等(4)
    JS图片延迟加载分析及简单的demo
    SVN服务器搭建和使用(三)(转载)
    SVN服务器搭建和使用(二)(转载)
    SVN服务器搭建和使用(一)(转载)
    JAVA基础学习之String、StringBuffer、StringBuilder、基本数据类型的使用、整形进制转换、集合Collection、Vector、ArrayList、LinkedList、HashSet、TreeSet等(3)
    Entity FrameWork 中使用Expression<Func<T,true>>访问数据库性能优化
    JAVA基础学习之throws和throw的区别、Java中的四种权限、多线程的使用等(2)
  • 原文地址:https://www.cnblogs.com/HuangYJ/p/14031237.html
Copyright © 2020-2023  润新知