算法题解(动态规划篇)

动态规划理论基础

什么是动态规划?

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。


对于动态规划问题,有如下五步曲:

在这里插入图片描述

509. 斐波那契数 - 12.21

509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给你 n ,请计算 F(n) 。

在这里插入图片描述


解法一:递归

class Solution {
    public int fib(int n) {
        int sum = 0;
        if(n == 0) return 0;
        if(n == 1) return 1;
        if(n > 1){
            sum =  fib(n-1) + fib(n-2);
        }
        return sum;
    }
}

在这里插入图片描述


解法二:动态规划

class Solution {
    public int fib(int n) {
        if(n <= 1) return n;
        //定义dp数组
        int[] dp = new int[n+1];
        //初始化
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; i++){ //注意从2开始
            dp[i] = dp[i-1] + dp[i-2]; //状态转移方程
        }
        return dp[n];
    }
}

在这里插入图片描述


剑指 Offer 10- II. 青蛙跳台阶问题 - 12.3

剑指 Offer 10- II. 青蛙跳台阶问题

在这里插入图片描述


解析:首先考虑n等于0、1、2时的特殊情况,f(0) = 0 f(1) = 1 f(2) = 2 其次,当n=3时,青蛙的第一跳有两种情况:跳1级台阶或者跳两级台阶,假如跳一级,那么 剩下的两级台阶就是f(2);假如跳两级,那么剩下的一级台阶就是f(1),因此f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2),以此类推…

class Solution {
    public int numWays(int n) {
        int former1 = 1;
        int former2 = 2;
        int sum = 0;
        if(n == 0) return 1;
        if(n == 1) return 1;
        if(n == 2) return 2;
        else{
            //动态规划,当n = 3时,f(3)=f(2)+f(1)  当n = 4时,f(4) = f(3) +f(2)
            for(int i=3; i<=n; i++){
                sum = (former1 + former2) % 1000_000_007;
                former1 = former2;
                former2 = sum;
            }
            return sum;
        }
    }
}

在这里插入图片描述

70. 爬楼梯 - 12.21

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。
在这里插入图片描述


解析:动态规划

从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!

所以dp[i] = dp[i - 1] + dp[i - 2] 。

class Solution {
    public int climbStairs(int n) {
        if(n <= 2) return n;
        //构建dp数组
        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];
    }
}

在这里插入图片描述

518. 零钱兑换 II - 12.22

518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

在这里插入图片描述


解析:完全背包应用

1、确定dp数组以及下标的含义

dp[j]:凑成总金额j的货币组合数为dp[j]

2、确定递推公式

dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。

所以递推公式:dp[j] += dp[j - coins[i]];

3、dp数组如何初始化

首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。

从dp[i]的含义上来讲就是,凑成总金额0的货币组合数为1。

4、确定遍历顺序

外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)

5、举例推导dp数组

输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下:

在这里插入图片描述
最后红色框dp[amount]为最终结果。


class Solution {
    public int change(int amount, int[] coins) {
        //dp数组
        int[] dp = new int[amount + 1];
        //初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
        dp[0] = 1;
        //先遍历硬币(物品)
        for(int i=0; i<coins.length; i++){
            //再遍历总金额(背包)
            for(int j=coins[i]; j<=amount; j++){
                //递推表达式
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];

    }
}

在这里插入图片描述

322. 零钱兑换 - 12.22

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

在这里插入图片描述


解析:完全背包应用

class Solution {
    public int coinChange(int[] coins, int amount) {
        //最大值
        int max = Integer.MAX_VALUE;
        //dp[j]:凑成总金额所需的最少的硬币个数
        int[] dp = new int[amount+1];
        //初始化为最大值
        for(int i=0; i<dp.length; i++){
            dp[i] = max;
        }
        //当金额为0时需要的硬币数目为0
        dp[0] = 0;
        //
        for(int i=0; i<coins.length; i++){
            //正序遍历:完全背包每个硬币可以选择多次
            for(int j = coins[i]; j <= amount; j++){
                //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                if(dp[j-coins[i]] != max){
                    //选择硬币数目最小的情况
                    dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
                }
            }
        }
        //如果没有任何一种硬币组合能组成总金额,返回 -1;否则返回dp[amount]
        return dp[amount] == max ? -1 : dp[amount];
    }
}

在这里插入图片描述

62. 不同路径 - 12.23

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

在这里插入图片描述


解析:动态规划,二维数组dp

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];

        //第一行都赋予 1
        for(int i=0; i<n; i++) dp[0][i] = 1;
        //第一列都赋予 1
        for(int i=0; i<m; i++) dp[i][0] = 1;

        //从上到下,从左到右的顺序
        for(int i=1; i<m; i++){
            for(int j=1; j<n; j++){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

63. 不同路径 II - 12.23

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

在这里插入图片描述


解析:遇到障碍时,dp[i][j]保持0。

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length; //行
        int n = obstacleGrid[0].length; //列
        int[][] dp = new int[m][n];
        for(int i=0; i<m; i++){
            //如果遇到障碍就停止赋值
            if(obstacleGrid[i][0] == 1) break;
            dp[i][0] = 1;
        }
        for(int j=0; j<n; j++){
            //如果遇到障碍就停止赋值
            if(obstacleGrid[0][j] == 1) break;
            dp[0][j] = 1;
        }
        for(int i=1; i<m; i++){
            for(int j=1; j<n; j++){
                //如果遇到障碍就 跳过推导
                if(obstacleGrid[i][j] == 1) continue;
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

在这里插入图片描述

42. 接雨水 - 2.10

42. 接雨水

在这里插入图片描述


解析:我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。

当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。

即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]);

从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]);

//动态规划
class Solution { 
    public int trap(int[] height) {
        int len = height.length; //长度
        if(len == 0){
            return 0;
        }

        //从左往右遍历,记录左边最高值
        int[] left_max = new int[len];
        left_max[0] = height[0];
        for(int i=1; i<len; i++){
            left_max[i] = Math.max(left_max[i-1],height[i]);
        }

        //从右往左遍历,记录右边最高值
        int[] right_max = new int[len];
        right_max[len-1] = height[len-1];
        for(int j=len-2; j>=0; j--){
            right_max[j] = Math.max(right_max[j+1],height[j]);
        }

        //计算接水量
        int sum = 0;
        for(int k=0; k<len; k++){
            sum += Math.min(left_max[k],right_max[k]) - height[k];
        }

        return sum;

    }
}

在这里插入图片描述

84. 柱状图中最大的矩形 - 2.10

84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

在这里插入图片描述


解析: 动态规划,本题要记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。所以需要循环查找,也就是下面在寻找的过程中使用了while。

class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;

        // 从左往右,记录左边第一个小于该柱子的下标
        int[] minLeftIndex = new int[len];
        minLeftIndex[0] = -1;
        for(int i=1; i<len; i++){
            int temp = i-1;
            //注意这里是用while来查找
            while(temp >= 0 && heights[temp] >= heights[i]) temp = minLeftIndex[temp];
            minLeftIndex[i] = temp;
        }

        // 从右往左,记录右边第一个小于该柱子的下标
        int[] minRightIndex = new int[len];
        minRightIndex[len-1] = len;
        for(int i=len-2; i>=0; i--){
            int temp = i+1;
            while(temp < len && heights[temp] >= heights[i]) temp = minRightIndex[temp];
            minRightIndex[i] = temp;
        }
        
        // 求和
        int res = 0;
        for(int i=0; i<len; i++){
            int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1);
            res = Math.max(res, sum);
        }

        return res;
    }
}

在这里插入图片描述

剑指 Offer 10- I. 斐波那契数列

剑指 Offer 10- I. 斐波那契数列
在这里插入图片描述


解析:动态规划,构建dp数组,初始化然后推导出递推表达式。

class Solution { 
    public int fib(int n) {
        if(n <= 1) return n;
        int[] dp = new int[n+1]; //dp数组
        dp[0] = 0; //初始化
        dp[1] = 1;
        for(int i=2; i<=n; i++){ //注意这里是 <=n
            //递推表达式
            dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
        }
        return dp[n];
    }
}

在这里插入图片描述

剑指 Offer 10- II. 青蛙跳台阶问题

剑指 Offer 10- II. 青蛙跳台阶问题

在这里插入图片描述


解析:动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2);依次类推。

class Solution {
    public int numWays(int n) {
        if(n <= 1) return 1;
        if(n == 2) return 2;
        int sum = 0;
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        //动态规划,当n = 3时,f(3)=f(2)+f(1)  当n = 4时,f(4) = f(3) +f(2)
        for(int i=3; i<=n; i++){
            dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
        }
        return dp[n];

    }
}

在这里插入图片描述

剑指 Offer 63. 股票的最大利润

剑指 Offer 63. 股票的最大利润

在这里插入图片描述


解析:动态规划,动态取买入最小值和卖出最大值

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        if(len <= 1) return 0;
        int minCost = Integer.MAX_VALUE;
        int max = 0;
        for(int price : prices){
            minCost = Math.min(minCost, price); //取买入最小价格
            max = Math.max(max, price-minCost); //取卖出最大值
        }
        return max;
    }
}

在这里插入图片描述

剑指 Offer 42. 连续子数组的最大和

剑指 Offer 42. 连续子数组的最大和

在这里插入图片描述


解析:

在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;
        //dp[i]表示以元素 nums[i] 为结尾的连续子数组最大和。
        int[] dp = new int[len+1];
        dp[0] = nums[0];
        //存储较大的 子数组最大和
        int res = dp[0]; 
        for(int i=1; i<=len-1; i++){
            dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

在这里插入图片描述

剑指 Offer 47. 礼物的最大价值

剑指 Offer 47. 礼物的最大价值
在这里插入图片描述


解析:dp(i,j) 代表从棋盘的左上角开始,到达单元格 (i,j)(i,j) 时能拿到礼物的最大累计价值。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxValue(int[][] grid) {
        int rows = grid.length; //行
        int cols = grid[0].length; //列
        int[][] dp = new int[rows][cols];
        dp[0][0] = grid[0][0];
        //行赋值
        for(int i=1; i<rows; i++){
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        //列赋值       
        for(int j=1; j<cols; j++){
            dp[0][j] = dp[0][j-1] + grid[0][j];
        }      
        //从上到下,从左到右的顺序
        for(int i=1; i<rows; i++){
            for(int j=1; j<cols; j++){
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }
        return dp[rows-1][cols-1];

    }
}

在这里插入图片描述

剑指 Offer 46. 把数字翻译成字符串

剑指 Offer 46. 把数字翻译成字符串

在这里插入图片描述


解析:先将数字转为字符串,然后定义dp[i]数组,表示前i个数字一共有多少种不同的翻译方法。

参考:把数字翻译成字符串 | 图解DP | 最清晰易懂的题解

class Solution {
    public int translateNum(int num) {
        //将数字转为字符串
        String s = String.valueOf(num);
        int n = s.length();
        //dp[i]表示 前i个数字共有多少种不同的翻译
        int[] dp = new int[n+1];
        dp[0] = 1;
        for(int i=1; i<=n; i++){
            //单独翻译s[i]
            dp[i] = dp[i-1];
            if(i > 1){
                int temp = (s.charAt(i-2) - '0')*10 + (s.charAt(i-1) - '0');
                if(temp >= 10 && temp <= 25){
                    dp[i] = dp[i] + dp[i-2]; //组合翻译
                }
            }
        }
        return dp[n];
    }
}

在这里插入图片描述

剑指 Offer 48. 最长不含重复字符的子字符串

剑指 Offer 48. 最长不含重复字符的子字符串

在这里插入图片描述


解法一:滑动窗口

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if(s == null || s.length()<=0) return 0;
        int leftIndex = -1; //定义左边界,注意这里是-1
        int res = 0;
        Map<Character, Integer> map = new HashMap<>();
        char[] ch = s.toCharArray();
        for(int i=0; i<s.length(); i++){
            if(map.containsKey(ch[i])){ //当前数字 在之前遍历时出现过,就更新左边界
                leftIndex = Math.max(leftIndex, map.get(ch[i]));
            }
            map.put(ch[i], i); //添加元素
            res = Math.max(res, i-leftIndex); //计算当前最大长度
        }
        return res;

    }
}

在这里插入图片描述

解法二:动态规划+哈希表

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> dic = new HashMap<>();
        int res = 0, tmp = 0;
        for(int j = 0; j < s.length(); j++) {
            int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
            dic.put(s.charAt(j), j); // 更新哈希表
            // dp[j - 1] -> dp[j]
            tmp = tmp < j - i ? tmp + 1 : j - i; 
            // max(dp[j - 1], dp[j])
            res = Math.max(res, tmp); 
        }
        return res;
    }
}

在这里插入图片描述

剑指 Offer 19. 正则表达式匹配

剑指 Offer 19 正则表达式匹配

在这里插入图片描述


解析:

参考:正则表达式匹配(动态规划,清晰图解)

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length() + 1, n = p.length() + 1;
        boolean[][] dp = new boolean[m][n];
        dp[0][0] = true;
        // 初始化首行
        for(int j = 2; j < n; j += 2)
            dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
        // 状态转移
        for(int i = 1; i < m; i++) {
            for(int j = 1; j < n; j++) {
                if(p.charAt(j - 1) == '*') {
                    if(dp[i][j - 2]) dp[i][j] = true;                                            // 1.
                    else if(dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) dp[i][j] = true; // 2.
                    else if(dp[i - 1][j] && p.charAt(j - 2) == '.') dp[i][j] = true;             // 3.
                } else {
                    if(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) dp[i][j] = true;  // 1.
                    else if(dp[i - 1][j - 1] && p.charAt(j - 1) == '.') dp[i][j] = true;         // 2.
                }
            }
        }
        return dp[m - 1][n - 1];
    }
}

在这里插入图片描述

解法二:

参考:https://www.iamshuaidi.com/2032.html

class Solution {
    public boolean isMatch(String s, String p){
        if(s == null || p == null)
            return false;
        int len_s = s.length();
        int len_p = p.length();
        //存放状态,默认初始值都是 false。
        boolean[][] dp = new boolean[len_s+1][len_p+1];
        //初始化
        dp[0][0] = true;
        for(int j = 1; j <= len_p; j++){
            if(p.charAt(j-1) == '*')
                dp[0][j] = dp[0][j-2];
        }
        for(int i = 1; i <= len_s; i++){
            for(int j = 1; j <= len_p; j++){
                //如果不为‘*’且匹配
                if(p.charAt(j-1)=='.'||p.charAt(j-1)==s.charAt(i-1))
                    dp[i][j] = dp[i-1][j-1];
                //如果是 *
                else if(p.charAt(j-1)=='*'){
                    //如果p[j]前面的字符p[j-1]与s[i]字符不匹配,则匹配0个
                    if(j!=1&&p.charAt(j-2)!='.'&&p.charAt(j-2)!=s.charAt(i-1)){
                        dp[i][j] = dp[i][j-2];
                    }else{
                        //否则有三种情况:
                        //匹配0个,匹配1个,匹配多个
                        dp[i][j] = dp[i][j-2] || dp[i][j-1]||dp[i-1][j];
                    }
                }
            }
        }
        return dp[len_s][len_p];
    }
}

剑指 Offer 49. 丑数

剑指 Offer 49. 丑数

在这里插入图片描述


解析:

参考:丑数(动态规划,清晰图解)

class Solution {
    public int nthUglyNumber(int n) {
        if(n == 1) return 1;
        int a = 0, b = 0, c = 0;
        int[] dp = new int[n]; //dp[i]代表第 i + 1 个丑数;
        dp[0] = 1;
        for(int i=1; i<n; i++){
            int n2 = dp[a] * 2;
            int n3 = dp[b] * 3;
            int n5 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n2, n3), n5); //取最小值
            if(dp[i] == n2) a++; //更新索引
            if(dp[i] == n3) b++;
            if(dp[i] == n5) c++;
        }
        return dp[n-1];

    }
}

在这里插入图片描述

剑指 Offer 60. n个骰子的点数

剑指 Offer 60. n个骰子的点数
在这里插入图片描述


解析:

参考: n 个骰子的点数(动态规划,清晰图解)

在这里插入图片描述
在这里插入图片描述

class Solution {
    public double[] dicesProbability(int n) {
        //因为最后的结果只与前一个动态转移数组有关,所以这里只需要设置一个一维的动态转移数组
        //原本dp[i][j]表示的是前i个骰子的点数之和为j的概率,现在只需要最后的状态的数组,所以就只用一个一维数组dp[j]表示n个骰子下每个结果的概率。
        //初始是1个骰子情况下的点数之和情况,就只有6个结果,所以用dp的初始化的size是6个
        double[] dp = new double[6];
        //只有一个数组
        Arrays.fill(dp,1.0/6.0);
        //从第2个骰子开始,这里n表示n个骰子,先从第二个的情况算起,然后再逐步求3个、4个···n个的情况
        //i表示当总共i个骰子时的结果
        for(int i=2;i<=n;i++){
        //每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
        //比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
            //当有i个骰子时的点数之和的值数组先假定是temp
            double[] temp = new double[5*i+1];
            //从i-1个骰子的点数之和的值数组入手,计算i个骰子的点数之和数组的值
            //先拿i-1个骰子的点数之和数组的第j个值,它所影响的是i个骰子时的temp[j+k]的值
            for(int j=0;j<dp.length;j++){
            //比如只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况,这里可能画一个K神那样的动态规划逆推的图就好理解很多
                for(int k=0;k<6;k++){
                    //这里记得是加上dp数组值与1/6的乘积,1/6是第i个骰子投出某个值的概率
                    temp[j+k]+=dp[j]*(1.0/6.0);
                }
            }
            //i个骰子的点数之和全都算出来后,要将temp数组移交给dp数组,dp数组就会代表i个骰子时的可能出现的点数之和的概率;用于计算i+1个骰子时的点数之和的概率
            dp = temp;
        }
        return dp;
    }   
}

在这里插入图片描述

198. 打家劫舍 - 3.5

在这里插入图片描述


解析:

在这里插入图片描述

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 0) return 0;
        //当小偷到达i号房屋时,能偷窃到的最高金额是dp[i]
        int[] dp = new int[nums.length+1];
        dp[0] = nums[0];
        if(nums.length < 2){ // 每次做数组判定时都需要做数组边界判定,防止越界
            return nums[0];
        }
        dp[1] = Math.max(nums[0], nums[1]);
        for(int i=2; i<nums.length; i++){
            //假如要不偷,那么就有 dp[i] = dp[i-1]。
            //假如要偷,那么意味着前面的那个房子不能偷,那么有 dp[i] = num[i] + dp[i-2].
            //故关系式为 dp[i] = max{dp[i-1], dp[i-2] + num[i]}.
            dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]);
        }
        return dp[nums.length-1];
    }
}

在这里插入图片描述

300. 最长递增子序列 - 3.7

300. 最长递增子序列
在这里插入图片描述


解析:
在这里插入图片描述
nums[5] = 3,既然是递增子序列,我们只要找到前面那些结尾比 3 小的子序列,然后把 3 接到最后,就可以形成一个新的递增子序列,而且这个新的子序列长度加一。

显然,可能形成很多种新的子序列,但是我们只选择最长的那一个,把最长子序列的长度作为 dp[5] 的值即可。

class Solution {
    public int lengthOfLIS(int[] nums) {
        //dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
        int[] dp = new int[nums.length + 1];
        //初始化为1
        Arrays.fill(dp, 1);
        for(int i=0; i<nums.length; i++){
            for(int j=0; j<i; j++){
                if(nums[i] > nums[j]){
                    //若发现当前元素nums[i]大于nums[j],则取(dp[i], dp[j]+1)的较大值
                    dp[i] = Math.max(dp[i], dp[j]+1); 
                    
                }
            }
        }
        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            res = Math.max(res, dp[i]); //更新最大长度
        }        
        return res;
    }
}

931. 下降路径最小和 - 3.7

931. 下降路径最小和
在这里插入图片描述
在这里插入图片描述


解析:

状态转移方程,对于nums[row][col]这个位置,其最小下降路径和为:
dp[row][col] = nums[row][col] + Math.min(nums[row+1][col-1], nums[row+1][col], nums[row+1][col+1])

class Solution {
   /**
     * 状态转移方程,对于nums[row][col]这个位置,其最小下降路径和为:
     * dp[row][col] = nums[row][col] + Math.min(nums[row+1][col-1], nums[row+1][col], nums[row+1][col+1])
     * base case为落到底时,注意检查col是否越界
     * TC=O(N^2),SC=O(N^2)
     */

    int[][] dp;
    public int minFallingPathSum(int[][] matrix) {
        int n = matrix.length; //行数
        dp = new int[n+1][n+1];
        for(int[] arr : dp){
            // 初始化dp数组
            Arrays.fill(arr, Integer.MAX_VALUE);
        }
        int min = Integer.MAX_VALUE;
        for(int i = 0; i < n; i++){
            // 逐个计算第一行的每个元素的最小下降路径和,取最小的一个
            min = Math.min(min, matrix(matrix, 0, i));
        }
        return min;
    }
    
    int matrix(int[][] matrix, int row, int col){
        int n = matrix.length;
        // base case
        // 列越界
        if(col < 0 || col >= n) return Integer.MAX_VALUE;
        // 落到底
        if(row == n - 1) return matrix[row][col];
        // 查询备忘录
        if(dp[row][col] != Integer.MAX_VALUE) return dp[row][col];
        int res = matrix[row][col] + Math.min(Math.min(matrix(matrix, row+1, col-1), matrix(matrix, row+1, col)), matrix(matrix, row+1, col+1));
        // 保存结果到备忘录
        dp[row][col] = res;
        return res;
    }
}

在这里插入图片描述

1143. 最长公共子序列 - 3.9

1143. 最长公共子序列
在这里插入图片描述


解析:动态规划

最长公共子序列 | 图解DP |

在这里插入图片描述

在这里插入图片描述

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        
        // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
        // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
        // base case: dp[0][..] = dp[..][0] = 0    
        int[][] dp = new int[len1+1][len2+1];

        for(int i=1; i<=len1; i++){
            for(int j=1; j<=len2; j++){
                // 现在 i 和 j 从 1 开始,所以要减一
                if(text1.charAt(i-1) == text2.charAt(j-1)){
                    // s1[i-1] 和 s2[j-1] 必然在 lcs 中
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[len1][len2];

    }
}

在这里插入图片描述

583. 两个字符串的删除操作 - 3.9

583. 两个字符串的删除操作

在这里插入图片描述


解析:先算出两个字符串最长公共字串的长度,再推导最小删除次数。

class Solution {
    public int minDistance(String word1, String word2) {
        int len1 = word1.length();
        int len2 = word2.length();
        //算出最长公共字串的长度
        int num = longestCommonSubsequence(word1, word2);
        //推导删除的次数
        return len1 - num + len2 - num;
    }

    //求最长公共字串的长度
    public int longestCommonSubsequence(String text1, String text2) {
        int len1 = text1.length();
        int len2 = text2.length();
        
        // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
        // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
        // base case: dp[0][..] = dp[..][0] = 0    
        int[][] dp = new int[len1+1][len2+1];

        for(int i=1; i<=len1; i++){
            for(int j=1; j<=len2; j++){
                // 现在 i 和 j 从 1 开始,所以要减一
                if(text1.charAt(i-1) == text2.charAt(j-1)){
                    // s1[i-1] 和 s2[j-1] 必然在 lcs 中
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[len1][len2];   
    } 
}

712. 两个字符串的最小ASCII删除和 - 3.9

712. 两个字符串的最小ASCII删除和

在这里插入图片描述


解法一:

class Solution {
    public int minimumDeleteSum(String s1, String s2) {
        int n = s1.length();
        int m = s2.length();
        int sum = 0;
        //求字符串1的总值
        for(int i = 0;i<n;i++){
            sum += s1.charAt(i);
        }
        //求字符串2的总值
        for(int j = 0;j<m;j++){
            sum += s2.charAt(j);
        }

        //求最长公共子串的长度
        int[][] dp = new int[n+1][m+1];
        for(int i = 1;i<=n;i++){
            for(int j = 1;j<=m;j++){
                if(s1.charAt(i-1)==s2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1]+s1.charAt(i-1);
                }else{
                    dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        //字符串1 加 字符串2,再减去 两倍的最长公共子串
        return sum - 2*dp[n][m];
    }
}

在这里插入图片描述

解法二:

class Solution {
    // 备忘录
    int memo[][];
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        // 备忘录值为 -1 代表未曾计算
        memo = new int[m][n];
        for (int[] row : memo) 
            Arrays.fill(row, -1);

        return dp(s1, 0, s2, 0);
    }

    // 定义:将 s1[i..] 和 s2[j..] 删除成相同字符串,
    // 最小的 ASCII 码之和为 dp(s1, i, s2, j)。
    int dp(String s1, int i, String s2, int j) {
        int res = 0;
        // base case
        if (i == s1.length()) {
            // 如果 s1 到头了,那么 s2 剩下的都得删除
            for (; j < s2.length(); j++)
                res += s2.charAt(j);
            return res;
        }
        if (j == s2.length()) {
            // 如果 s2 到头了,那么 s1 剩下的都得删除
            for (; i < s1.length(); i++)
                res += s1.charAt(i);
            return res;
        }

        //查备忘录
        if (memo[i][j] != -1) {
            return memo[i][j];
        }

        if (s1.charAt(i) == s2.charAt(j)) {
            // s1[i] 和 s2[j] 都是在 lcs 中的,不用删除
            memo[i][j] = dp(s1, i + 1, s2, j + 1);
        } else {
            // s1[i] 和 s2[j] 至少有一个不在 lcs 中,删一个
            memo[i][j] = Math.min(
                s1.charAt(i) + dp(s1, i + 1, s2, j),
                s2.charAt(j) + dp(s1, i, s2, j + 1)
            );
        }
        return memo[i][j];
    }
}

在这里插入图片描述

72. 编辑距离 - 3.10

72. 编辑距离

在这里插入图片描述


解析:

在这里插入图片描述

class Solution {
    public int minDistance(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        // 定义:s1[0..i] 和 s2[0..j] 的最小编辑距离是 dp[i-1][j-1]
        int[][] dp = new int[m + 1][n + 1];
        // base case 
        for (int i = 1; i <= m; i++)
            dp[i][0] = i;
        for (int j = 1; j <= n; j++)
            dp[0][j] = j;
        // 自底向上求解
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s1.charAt(i-1) == s2.charAt(j-1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min(
                        dp[i - 1][j] + 1,  //删除
                        dp[i][j - 1] + 1,  //插入
                        dp[i - 1][j - 1] + 1  //替换
                    );
                }
            }
        }
        // 储存着整个 s1 和 s2 的最小编辑距离
        return dp[m][n];
    }

    int min(int a, int b, int c) {
        return Math.min(a, Math.min(b, c));
    }
}

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值