动态规划练习

基本动态规划:一维

爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs

很明显,这是一个斐波纳契数列,为什么怎么说呢?请看下面的讲解:
由于每次可以爬1或者2个台阶,所以求解n阶台阶有多少种方式可以到达,那么就需要看n - 2阶(爬2个台阶到达n阶)和n - 1阶(爬1个台阶到达n阶)有多少种到达方式,n阶台阶到达方式就是两者到达方式之和。

基于此,我们将定义一个状态方程dp,dp[ i ]表示的是第 i 阶台阶的到达方式。
状态转移方程: dp[ i ] = dp[ i - 1] (爬1个台阶到达 i 阶)+ dp[ i - 2](爬2个台阶到达i 阶)
由于n = 0的时候,虽然不需要爬台阶,但是为了方便计算,我们将其值赋值为1,当n = 1的时候,需要爬台阶,dp[ 1 ] = 1,当n = 2,那么就有dp[ 1 ] + dp[ 0 ],所以如果将dp[ 0 ]赋值为0的话,就会导致dp[ 2 ] = 1,从而错误,因此将dp[ 0 ]赋值为1,刚好和斐波纳契数列的公式相同

返回值: dp[ n ]

对应的代码:

class Solution {
    public int climbStairs(int n) {
         int[] dp = new int[n + 1];
         dp[0] = dp[1] = 1;
         for(int i = 2; i <= n; ++i){
             dp[i] = dp[i - 1] + dp[i - 2];
         }
         return dp[n];
    }
}

运行结果:
在这里插入图片描述
由于最后我们返回值是dp[n],所以我们定义一个变量,表示最后的结果f,f1,f2分别表示爬1个台阶到达第n个天界,爬2个台阶到达第n个台阶,所以f = f1 + f2,每次循环需要更新f1,f2,使得f2 = f1,f2 = f。
对应的代码:

class Solution {
    public int climbStairs(int n) {
         int f1 = 1,f2 = 1,f = 1;//将f值为1,是因为如果n等于1的时候,只有一种方式
         for(int i = 2; i <= n; ++i){
             f = f1 + f2;
             f1 = f2;
             f2 = f;
         }
         return f;
    }
}

运行结果:
在这里插入图片描述

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

1 <= nums.length <= 100
0 <= nums[i] <= 400
通过次数352,078提交次数692,067

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber

由于如果偷窃的是相邻的,那么就会想起警报。状态方程dp[ i ] 表示的是在下标为 i 的房子得到的最多金钱,如果不能在 i 这个房子偷窃,那么在这个房子中得到的金钱为dp[ i - 1],如果能够在 i 这个房子偷窃,表示前面一个房子 i - 1没有偷窃,此时在这个房子偷盗得到的钱为dp[ i - 2] + nums[ i ],这时候为了保证在这个房子得到的金钱最多,还需要在两者中取最大值,即dp[ i ] = Math.max(dp[ i - 1],dp[i - 2] + nums[ i ])

对应的代码:

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 1)//只有一个房子的时候,直接返回
           return nums[0];
        int[] dp = new int[nums.length];
        dp[0] = nums[0];//在第一个房子得到的金钱
        dp[1] = Math.max(nums[0],nums[1]);//第二个房子得到的最多金钱
        for(int i = 2; i < nums.length; ++i){
            dp[i] = Math.max(dp[i - 2] + nums[i],dp[i - 1]);
        }
        return dp[nums.length - 1];
    }
}

运行结果:
在这里插入图片描述

等差数列划分

如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。子数组 是数组中的一个连续序列

示例 1:
输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。

示例 2:
输入:nums = [1]
输出:0

提示:
1 <= nums.length <= 5000
-1000 <= nums[i] <= 1000

方法一:暴力解决
由于是等差数列,那么nums[ i ] - nums[i - 1] = nums[i + 1] - nums[ i ],
所以首先获取nums[ i + 1] - nums[ i ]的差div,然后判断nums[ i + 1]后面的数nums[ i + 2] - nums[ i + 1],如果是,就算一种连续的等差子数列。
对应的代码:

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int res = 0,i,k,div;
        for(i = 0; i < nums.length - 2; ++i){
            div = nums[i + 1] - nums[i];
            for(k = i + 2; k < nums.length && nums[k] - nums[k - 1] == div; ++k){
                res++;
            }
        }
        return res;
    }
}

运行结果:
在这里插入图片描述
既然这里说动态规划,当然是用动态规划来解决啦。
方法二 : 动态规划
状态方程:dp[ i ]表示以nums[ i ]结尾的连续等差子数组有多少种。初始值为0,表示以nums[ i ]结尾的来纳许等差子数组有0种
转移方程:dp[ i ] = dp[ i - 1] + 1。
返回值: 由于dp是每一个数字结尾构成的连续子数组的值,所以需要遍历这个数组,将他们的值进行累加才是最后的结果。

状态方程dp[ i ]表示以nums[ i ]结尾的连续等差子数组,那么应该是dp[ i ]等于dp[ i - 1],为什么还要加上1呢?难道是因为多了一个数字,才加上1的吗?

可以这样说,因为如果dp[ i - 1]结尾的话形成的等差连续子数组有n种,并且dp[ i ]也可以构成连续的等差子数组的话,那么dp[ i ]比dp[ i - 1]多了一种情况,[i - 2,i ]这个连续等差子数组就是dp[ i ]比dp[ i - 1]多出来的情况,所以需要加1.
对应代码:

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int[] dp = new int[nums.length];
        int i,res = 0;
        for(i = 2; i < nums.length; ++i){
            //从2开始遍历,因为要求至少3个元素
            if(nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]){
                dp[i] = dp[i - 1] + 1;
                res += dp[i];
            }
        }
        return res;
    }
}

运行结果:
在这里插入图片描述

基本动态规划:二维

最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步

示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12

提示:

m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-path-sum

由于是从左上角到右上角,所以定义一个二维数组dp,dp[ i ][ j ]表示到达[ i ][ j ]的最小的路径总和
状态转移方程:

  • 如果当前位置在第一列,那么dp[ i ][ j ] = dp[ i - 1][ j ] + grid[ i ][ j ]
  • 如果当前位置在第一行,那么dp[ i ][ j ] = dp[ i ][ j - 1]
  • 否则,dp[ i ][ j ] = Math.min(dp[ i - 1][ j ],dp[ i ][ j - 1]) + grid[ i ][ j ]
    返回值: dp[col - 1][ row - 1],表示最后到达右下角[col - 1,row - 1]这个位置的最小路径和
    对应的代码:
class Solution {
    public int minPathSum(int[][] grid) {
        if(grid == null || grid.length == 0 || grid[0].length == 0)
           return 0;
        int i,j,col = grid.length,row = grid[0].length;
        int[][] dp = new int[col][row];
        dp[0][0] = grid[0][0];
        for(i = 1; i < row; ++i){
            dp[0][i] = dp[0][i - 1] + grid[0][i];
        }
        for(i = 1; i < col; ++i){
            dp[i][0] = grid[i][0] + dp[i - 1][0];
        }
        for(i = 1; i < col; ++i){
            for(j = 1; j < row; ++j){
                dp[i][j] = Math.min(dp[i - 1][j],dp[i][j - 1]) + grid[i][j];
            }
        }
        return dp[col - 1][row - 1];
    }
}

运行结果:
在这里插入图片描述

01 矩阵

给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离
两个相邻元素间的距离为 1 。

示例 1:
输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]

示例 2:
输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]

提示:

m == mat.length
n == mat[i].length
1 <= m, n <= 104
1 <= m * n <= 104
mat[i][j] is either 0 or 1.
mat 中至少有一个 0
来源:https://leetcode-cn.com/problems/01-matrix/

方法一:广度优先搜索
基本说明:
1、如果当前位置mat[ i ][ j ]的值为0,那么需要将dp[ i ][ j ] 赋值为0,然后需要将4个方向的位置遍历,如果其中位置的值dp,假设是dp[ i - 1][ j ]比level小,那么需要更新dp[ i - 1][ j ],然后将其压入到队列中
2、因为更新了dp[ i - 1][ j ],所以也需要更新和dp[ i - 1][ j ]相邻的,所以有了while操作。

class Solution {
    /*
    如果利用的是广度优先搜索,那么首先需要找到为0的数,然后将它四个方向上没有被访问过的
    1添加到队列中,统计他们到当前这个位置的距离。dp[i][j] = Math.min(dp[i][j],levle);
*/
    int[] move_x = new int[]{0,-1,0,1};
    int[] move_y = new int[]{-1,0,1,0};
    public int[][] updateMatrix(int[][] mat) {
         int col = mat.length,row = mat[0].length,i,j,k,a,b,d,size,level;
         Queue<int[]> queue = new LinkedList<int[]>();
         int[] tmp;
         int[][] dp = new int[col][row];
         for(i = 0; i < col; ++i){
             for(j = 0; j < row; ++j){
                 dp[i][j] = Integer.MAX_VALUE;
             }
         }
         for(i = 0; i < col; ++i){
             for(j = 0; j < row; ++j){
                  if(mat[i][j] == 0){
                      dp[i][j] = 0;
                      level = 1;
                      offer(queue,mat,dp,i,j,col,row,level);
                      while(!queue.isEmpty()){
                          ++level;
                          size = queue.size();
                          for(k = 0; k < size; ++k){
                              tmp = queue.poll();
                              offer(queue,mat,dp,tmp[0],tmp[1],col,row,level);
                          }
                      }
                  }
             }
         }
         return dp;
    }
    public void offer(Queue<int[]> queue,int[][] mat,int[][] dp,int i,int j,int col,int row,int level){
        int d,a,b;
        //将和当前位置相邻的1,并且它的距离大于level的时候压入到队列中
        for(d = 0; d < 4; ++d){
            a = i + move_x[d];
            b = j + move_y[d];
            if(a < 0 || a >= col || b < 0 || b >= row || mat[a][b] == 0 || dp[a][b] <= level)
                continue;
            dp[a][b] = level;
            queue.offer(new int[]{a,b});
            
        }

    }
}

运行结果:
在这里插入图片描述
方法二:动态规划

对应代码:

class Solution {
    /*
    动态规划:利用的是两次遍历
    状态方程:dp[i][j]表示[i][j]这个位置的1距离最近的0的距离,初始值为Integer.MAX_VALUE,表示距离0为无限远
    转移方程:
    第一次从左上角向右下角遍历:比较的是左、上两个方向的,dp[i][j] = Math.min(dp[i][j],Math.min(dp[i - 1][j],dp[i][j - 1]) + 1)
    第二次从右下角向左上角遍历:比较的是右、下两个方向的,dp[i][j] = Math.min(dp[i][j],Math.min(dp[i + 1][j],dp[i][j + 1]) + 1)
    返回值:dp
    */

    public int[][] updateMatrix(int[][] mat) {
         int col = mat.length,row = mat[0].length,i,j;
         int[][] dp = new int[col][row];//表示距离最近的0的距离,初始为Integer.MAX_VALUE,表示无限远
         for(i = 0; i < col; ++i){
             for(j = 0; j < row; ++j){
                 dp[i][j] = Integer.MAX_VALUE - 1;
             }
         }
         //第一次遍历,从左上角向右下角遍历,将当前位置和左、上两个方位进行比较
         for(i = 0; i < col; ++i){
             for(j = 0; j < row; ++j){
                  if(mat[i][j] == 0){
                      dp[i][j] = 0;
                  }else{
                      if(i > 0)//和上边进行比较
                        dp[i][j] = Math.min(dp[i][j],dp[i - 1][j] + 1);
                      if(j > 0)//和左边进行比较
                        dp[i][j] = Math.min(dp[i][j],dp[i][j - 1] + 1);
                  }
             }
         }
         //第二次遍历,从右下角向右上角遍历,将当前位置和右下两个方位进行比较
         for(i = col - 1; i >= 0; --i){
             for(j = row - 1; j >= 0; --j){
                 if(dp[i][j] != 0){
                     if(i < col - 1)//和下边方位进行比较
                        dp[i][j] = Math.min(dp[i][j],dp[i + 1][j] + 1);
                     if(j < row - 1)//和右边方位进行比较
                        dp[i][j] = Math.min(dp[i][j],dp[i][j + 1] + 1);   
                 }
             }
         }
         return dp;
    }


}

运行结果:
在这里插入图片描述

最大正方形

在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积

示例 1:
输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4

示例 2:
输入:matrix = [[“0”,“1”],[“1”,“0”]]
输出:1

示例 3:
输入:matrix = [[“0”]]
输出:0

提示:

m == matrix.length
n == matrix[i].length
1 <= m, n <= 300
matrix[i][j] 为 ‘0’ 或 ‘1’

利用二维数组来寻找正方形或者长方形。
状态方程dp[ i ][ j ]表示以[ i ][ j ]为右下角的长方形或者正方形。
所以在本题中,状态方程同样是一个二维数组dp[ i ][ j ],表示以[ i ][ j ]为右下角的全部都是‘ 1 ’构成的正方向,它的值表示构成的正方形的最大边长k,所以dp[ i - 1][ j ],dp[ i - 1][ j - 1],dp[ i ][ j - 1]三者的值需要小于等于k - 1,当三者都为k - 1,那么就可以dp[ i ][ j ]的值为k,此时构成的正方形最大。从而得出状态转移方程为dp[ i ][ j ]的值是dp[ i - 1][ j ],dp[ i - 1][ j - 1],dp[ i ][ j - 1]三者的最小值 + 1

返回值:dp[ i ][ j ]中的最大值的平方

对应的代码:

class Solution {
    /*
    利用动态规划
    状态方程dp[i][j]表示以[i][j]为右下角的正方形或者长方形,在本题中,
    dp[i][j]表示以[i][j]为右下角的正方形,并且它的值表示正方形的长,因此
    如果值为0,表示没有办法构成正方形,否则,dp[i][j] = k,那么dp[i - 1][j],
    dp[i - 1][j - 1],dp[i][j - 1]三者小于等于k - 1。
    状态转移方程:dp[i][j] = Math.max(Math.max(dp[i - 1][j],Math.max(dp[i - 1][j - 1],dp[i][j - 1]))) + 1
    返回值:返回dp中的最大值的平方
    */
    public int maximalSquare(char[][] matrix) {
          int col = matrix.length,row = matrix[0].length,i,j,max;
          int max_side = 0;
          int[][] dp = new int[col][row];
          //将dp第一行、第一列进行初始化,其值等于matrix的第一行、第一列
          for(i = 0; i < row; ++i){
              dp[0][i] = matrix[0][i] - '0';
              if(dp[0][i] > max_side)
                max_side = dp[0][i];
          }
          for(i = 0; i < col; ++i){
              dp[i][0] = matrix[i][0] - '0';
              if(dp[i][0] > max_side)
                max_side = dp[i][0];
          }
          /*
          从[1][1]开始遍历,如果它的值是'1',那么dp[i][j]就等于
          Math.min(dp[i][j - 1],Math.min(dp[i - 1][j - 1],dp[i - 1][j]) 
          + 1
          */
          for(i = 1; i < col; ++i){
              for(j = 1; j < row; ++j){
                  if(matrix[i][j] == '1'){
                  /*
                  之所以不这样写,是因为尽管三者中含有0,但是由于是取
                  三者中的最小值+1,此时三者的最小值必然是0,最终得到的
                  dp[i][j]的值依旧是1
                      if(dp[i - 1][j] > 0 && dp[i - 1][j - 1] > 0 && dp[i][j - 1] > 0){
                        max = Math.min(dp[i - 1][j],dp[i - 1][j - 1]);
                        dp[i][j] = Math.min(dp[i][j - 1],max) + 1;
                      }else{
                        dp[i][j] = 1;
                      }
                      */
                      max = Math.min(dp[i - 1][j],dp[i - 1][j - 1]);
                      dp[i][j] = Math.min(dp[i][j - 1],max) + 1;
                      if(dp[i][j] > max_side)
                        max_side = dp[i][j];
                  }
              }
          }
          return max_side * max_side;
    }
}

运行结果:
在这里插入图片描述

分割类题型

完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9

提示:
1 <= n <= 104

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/perfect-squares

由于需要我们获取完全平方数,使得他们的和等于n,最后返回和等于n的完全平方个数最少个数。因此我们利用一维数组来实现本题。

  • 状态方程:dp[ i ]表示的是构成和为 i 的完全平方数的最少个数

  • 转移方程:dp[ i ] = Math.min(dp[ i ],dp[ i - j * j] + 1)
    状态方程之所以是这样,请看下面的解析过程:
    假设 j 、 k是一个完全平方数,并且 i = j * j + k * k,这时候构成和为 i 的完全平方数的个数dp[ i ] = dp[ j * j ] + dp[ k * k],由于k * k = i - j * j,所以就转换成了dp[ i ] = dp[ j * j] + dp[ i - j * j],但是这个和我们的转移方程并不一样啊,难道是我们的思路错了吗?,通过比较两个式子,差别就是dp[ j * j]这里,难道它的值一定是1?🤔🤔🤔答案是肯定的,因为我们再次回到状态方程的定义中来,状态方程dp[ i ]表示构成和为 i 的完全平方数的最小个数,所以dp[ j * j]的最小个数就是他自己这个数,所以必然为1,因此dp[ j * j ]的值必然为1

  • 返回值:返回dp[ n ],表示构成和为n的完全平方数的最小个数.

对应的代码:

class Solution {
    public int numSquares(int n) {
          int[] dp = new int[n + 1];
          dp[0] = 0;//需要将这一步初始化为0,因为有0个完全平方数的和等于n
          //将剩余的元素初始化为Integer.MAX_VALUE
          int i,j;
          for(i = 1; i <= n; ++i)
             dp[i] = Integer.MAX_VALUE;
          for(i = 1; i <= n; ++i){
             for(j = 1; j * j <= i; ++j){
                 /*
                 状态转移方程是dp[i] = Math.min(dp[i],dp[i - j * j] + 1)
                 假设j,k是构成i的完全平方数,这时候就有i = j ^2 + k ^ 2
                 因此dp[i]就转成了两个子问题,求解dp[j ^ 2] + dp[k ^ 2],
                 但是由于k ^ 2 = i - j ^ 2,所以转成了dp[j ^ 2] + dp[i - 
                 j ^ 2]因为dp[j * j]这个数中构成的完全平方数最小为1(dp[i 
                 - j * j] + dp[j * j]),所以dp[j * j]的值最小为1,因此得
                 出了状态转移方程dp[i] = Math.min(dp[i],1 + dp[i - j * 
                 j]),其中j * j <= i
                 */
                 dp[i] = Math.min(dp[i],dp[i - j * j] + 1);
             }
          }
          return dp[n];
    }
}

运行结果: 在这里插入图片描述

解码方法

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:

“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

示例 1:
输入:s = “12”
输出:2
解释:它可以解码为 “AB”(1 2)或者 “L”(12)。

示例 2:
输入:s = “226”
输出:3
解释:它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。

示例 3:
输入:s = “0”
输出:0
解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 ‘J’ -> “10” 和 ‘T’-> “20” 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。

示例 4:
输入:s = “06”
输出:0
解释:“06” 不能映射到 “F” ,因为字符串含有前导 0(“6” 和 “06” 在映射中并不等价)。

提示:
1 <= s.length <= 100
s 只包含数字,并且可能包含前导零。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/decode-ways

基本方法:
状态方程:dp[ i ]表示的是以 i 结尾的数字解码的字符串的情况有多少种。
状态转移方程:主要需要考虑两种情况:

  • 如果当前的字符不是0,那么可以直接拼接到字符串的后面,并没有考虑当前数字和前一个数字拼接的情况,这时候dp[ i ] = dp[ i - 1]。但是我们需要考虑的是
  • 如果当前的字符不是第一个字符,并且前一个字符不是’0’,那么就可以进行拼接了,如果拼接后的数字是一个大于等于1,小于等于26的数字,那么这时候,dp[ i ] += dp[ i - 2],因为将下标为i、i - 1拼接后有dp[ i - 2]中可能.
    返回值: dp[chars.length]
    对应代码:
class Solution {
    public int numDecodings(String s) {
       char[] chars = s.toCharArray();
       int[] dp = new int[chars.length + 1];
       dp[0] = 1;
       int i,sum;
       for(i = 1; i <= chars.length; ++i){
           /*
           if(chars[i - 1] != '0'){
               //如果当前的数字不是0,那么需要分几种情况进行讨论
               if(i == 1)//如果是第一个数,那么以它结尾的情况只有一种
                   dp[i] = 1;
               else{
                   dp[i] = dp[i - 1];//在直接将这个数字放到后面的时候,没有考虑前一个数字和这个数字拼接的情况
                   sum = (chars[i - 2] - '0') * 10 + (chars[i - 1] - '0');
                   if(chars[i - 2] != '0' && sum >= 1 && sum <= 26)
                   //如果前一个数字不为0,并且可以和这个数字拼接,那么还需要加上dp[i - 2]
                      dp[i] += dp[i - 2];
               }
           }
           上面的代码尽管考虑了如果前面的数字是0,当前的数字不为0,并且可以和前者不可以拼接的情况,
           但是缺少了一种情况,就是如果当前的数字尽管是0,直接放到字符串的后面明显不合理,但是
           如果0和前面一个数字进行拼接没有考虑进去,因此需要将第二个if放出来
           */
           if(chars[i - 1] != '0'){
           //如果当前的字符不为0,那么可以直接拼接到后面,一共有dp[i - 1]中可能
                dp[i] = dp[i - 1];
           }
           if(i > 1){
           /*
           如果前面一个字符chars[i - 2]不为0,那么考虑数字拼接的情况,此
           时如果拼接后的数字大于等于1,小于等于26,那么表示可以拼接,此时
           以chars[i - 1]结尾的情况还需要加上dp[i - 2]
           即便当前的字符为0,同样需要考虑和前面一个字符拼接的情况(上面代
           码错误的原因主要就在于忽略了这个原因)
           */
               sum = (chars[i - 2] - '0') * 10 + (chars[i - 1] - '0');
               if(chars[i - 2] != '0' && sum >= 1 && sum <= 26)
               //如果前一个数字不为0,并且可以和这个数字拼接,那么还需要加上dp[i - 2]
                  dp[i] += dp[i - 2];
           }
       }
       return dp[chars.length];
    }
}

运行结果:
在这里插入图片描述

单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。

示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-break

  • 状态方程:dp[ i ]表示的是以下标 i 结尾的子串是否在列表中
  • 状态转移方程:
    由于dp[ i ]表示的是以下标 i 对应的字符结尾的子串是否包含于列表中,所以需要将 i 从下标 0 开始遍历,同时要获取下标 i 的子串,所以利用双层嵌套循环,j 同样从0开始遍历,**substring(j, i + 1)是以下标 i 对应的字符结尾的子串,如果对应的子串含于列表中,**这时候需要分几种情况:
    ①如果 j 等于0,说明[ 0, i ]构成的字符串含于字典列表中,此时dp[ i ]为true
    ②否则 j 不等于0,并且dp[ j - 1]为true,才可以将dp[ i ]置为true。之所以在dp[ j - 1]为true的时候才将dp[ i ]置为true,是因为可能含有重复使用字符串s的字符的情况,例如catsand,[“cats”,“sand”],这时候如果以字符d结尾,就会存在sand的子串,含于字典列表中,此时的 j 对应的字符为s,但是由于dp[ j - 1]为false,所以此时为false
  • 返回值:dp[len - 1]

对应代码:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
           /*
           本题并不应该遍历字符串列表,然后字符串s调用indexOf(string s)来
           判断字符串s是否含有列表中的单词,因为有可能尽管含有,但是最后的
           结果返回为false的情况:bananapple,字符串列表["banana","apple"]
           如果遍历字符串列表,然后字符串s调用indexOf方法就会导致结果的错
           误,因此不可以这样做
           */
           int i,j,k,size = wordDict.size(),len = s.length();
           boolean[] dp = new boolean[len];
           String sub_string;
           for(i = 0; i < len; ++i){
               for(j = 0; j <= i; ++j){
                   sub_string = s.substring(j,i + 1);
                   if(wordDict.contains(sub_string)){
                       /*
                       如果当前这个子字符串包含于列表中,这时我们需要判断
                       j是否等于0,如果等于0,表示这个子字符串是从0开始的
                       否则,如果j不等于0,这时候需要保证dp[j - 1]为true
                       的时候才可以将dp[i]置为true
                       */
                       if(j == 0 || dp[j - 1]){
                          dp[i] = true;
                          break;
                       }
                   }
               }
           }
           return dp[len - 1];
    }
}

运行结果:
在这里插入图片描述

子序列问题

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

1 <= nums.length <= 2500
-104 <= nums[i] <= 104

状态方程:dp[ i ]表示的是以下标 i 对应的数字结尾的最长的严格递增的子序列的长度
转移方程:一开始将dp[ i ]初始化为1,表示每一个数字结尾的最长的严格递增的子序列长度为1(也就是自己这个数),如果在这个数(下标 i 对应的数字)的前面(也就是 下标 j 对应的数)nums[ j ]小于nums[ i ],那么这时候两者将会形成一个严格递增的子序列,所以dp[ i ] = dp[ j ] + 1,由于是要取以数字nums[ i ]结尾的最长的严格递增子序列的长度,所以dp[ i ] = Math.max(dp[ i ],dp[ j ] + 1)
返回值:dp中的最大值
对应的代码:

class Solution {
    public int lengthOfLIS(int[] nums) {
           int[] dp = new int[nums.length];
           dp[0] = 1;
           int i,j,res = 1;
           for(i = 1; i < nums.length; ++i){
               dp[i] = 1;
               for(j = 0; j < i; ++j){
                  if(nums[i] > nums[j])
                    dp[i] = Math.max(dp[i],dp[j] + 1);
               }
               res = Math.max(dp[i],res);
           }
           return res;
    }
  
}

运行结果:
在这里插入图片描述

最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列

示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-common-subsequence

状态方程:dp[ i ][ j ]表示的是第一个字符串到达第 i 个字符为止,第二个字符串到达第 j 个字符为止的最长公共序列
转移方程:

  • i = 0或者j = 0,那么dp[ i ][ j ]为0,因为第0个字符是一个空字符串啊
  • 否则,如果i、j对应的字符相等,那么dp[ i ][ j ] = dp[ i - 1][ j - 1] + 1
  • 否则,i、j对应的字符不相等,此时dp[ i ][ j ]取决于dp[ i - 1][ j ],dp[ i ][ j - 1],因为状态方程是第一个字符串到达第 i 个字符为止,第二个字符串到达第 j 个字符为止的最长公共序列,所以只能是减,因此在两者对应的字符不相等的情况下,dp[ i ][ j ] = Math.max(dp[ i - 1][ j ],dp[ i ][ j - 1])

返回值:dp[m][n],其中m、n分别对应的是第一个字符串、第二个字符串的长度

对应的代码:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        char[] chars1,chars2;
        chars1 = text1.toCharArray();
        chars2 = text2.toCharArray();
        int i,j,m = chars1.length,n = chars2.length;
        int[][] dp = new int[m + 1][n + 1];
        for(i = 1; i <= m; ++i){
            for(j = 1; j <= n; ++j){
                if(chars1[i - 1] == chars2[j - 1])
                  dp[i][j] = dp[i - 1][j - 1] + 1;
                else{
                    dp[i][j] = Math.max(dp[i][j - 1],dp[i - 1][j]);
                }
            }
        }
        return dp[m][n];
       
    }
   
}

运行结果:
在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值