LeetCode刷题之动态规划(一)

1,450 阅读21分钟

动态规划就是暴力递归的优化,并不是一种数据结构,因为暴力递归过程有很多重复计算的部分。
解题方法:
1.如果第一眼就能看出来转移方程,那么直接根据转移方程解答;
2.如果一下子看不出动态规划解法,先写出暴力递归做法,然后再由暴力递归改成动态规划。 做法:找出变量,画出n维图,解答。

斐波那契数列

70. 爬楼梯(Easy)

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

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

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

示例 1:

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

  1. 1 阶 + 1 阶
  2. 2 阶

示例 2:

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

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

解法一:暴力递归,时间复杂度为2^n

class Solution {
    public int climbStairs(int n) {
        int result = climb_stairs(0, n);
        return result;
    }

    //跳之前,可以选择一步或者两步
    private int climb_stairs(int i, int n) {
        //i表示到第i个台阶,超过n时,不需要往上走了
        if (i > n){
            return 0;
        }
        //成功走到最后一个阶梯时,表示这是一种解法
        if (i == n){
            return 1;
        }
        return climb_stairs(i + 1, n) + climb_stairs(i + 2, n);
    }
}

解法二:在暴力递归基础上可以用一个数组记录哪些是已经走过的,加快执行速度

class Solution {
    public int climbStairs(int n) {
        //用来记录的数组
        int memo[] = new int[n + 1];
        return climb_Stairs(0, n, memo);
    }
    
    public int climb_Stairs(int i, int n, int memo[]) {
        if (i > n) {
            return 0;
        }
        if (i == n) {
            return 1;
        }
        //如果数组中已经存在,直接返回,不需要进行递归
        if (memo[i] > 0) {
            return memo[i];
        }
        memo[i] = climb_Stairs(i + 1, n, memo) + climb_Stairs(i + 2, n, memo);
        return memo[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 == 1){
            return 1;
        }
        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];
    }
}

198. 打家劫舍(Easy)

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

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

示例 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 。

题解:第一眼看不出动态规划做法,那么先写暴力递归。

  • 要求前n家最多能偷多少金额,并且知道前n-1和前n-2家的最大金额,那么第n家就可以选择偷或不偷,比较(第n家加上前n-2家)与(前n-1家)的大小即可得出答案。
  • 根据这个思路就可以写出暴力递归解答
class Solution {
    public int rob(int[] nums) {
        return robHelper(nums, nums.length);
    }

    private int robHelper(int[] nums, int n) {
        //base case
        if (n == 0){
            return 0;
        }
        if (n == 1){
            return nums[0];
        }
        //nums的最后一个下标是n-1
        return Math.max(robHelper(nums, n - 1), robHelper(nums, n - 2) + nums[n - 1]);
    }
}

解法二:暴力递归是肯定会超出时间的,重复了很多计算,可以用一个数组来记录哪些访问过的

public class Solution {
    public int rob(int[] nums) {
        //数组大小为length+1是因为一共有0--n
        int[] map = new int[nums.length + 1];
        Arrays.fill(map, -1);
        return robHelper(nums, nums.length, map);
    }

    private int robHelper(int[] nums, int n, int[] map) {
        //base case
        if (n == 0){
            return 0;
        }
        if (n == 1){
            return nums[0];
        }
        //访问过就直接返回,因为数据无后效性
        if (map[n] != -1){
            return map[n];
        }
        //nums的最后一个下标是n-1
        int res = Math.max(robHelper(nums, n - 1, map), robHelper(nums, n - 2, map) + nums[n - 1]);
        map[n] = res;
        return res;
    }
}

解法三:改成动态规划版本,写出来了暴力递归版本做法,就可以根据暴力递归改成动态规划。

  • 先找变量:i(第几家),sum(偷得金额)
  • 固定的变量:nums
  • 根据递归写法找出普遍的第i个的关系式:dp[n] = Math.max(dp[n - 2] + nums[n - 1], dp[n - 1] ),一维,遍历所有的nums,找出最大
public class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0){
            return 0;
        }
        if (n == 1){
            return nums[0];
        }
        
        int[] dp = new int[n + 1];
        //初始值
        dp[0] = 0;
        dp[1] = nums[0];
        //从第二家开始
        for (int i = 2; i <= n; i++) {
            //前j家的最大金额
            dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
        }
        return dp[n];
    }
}

213. 打家劫舍 II(Medium)

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

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

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

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

题解一:

  • 先写出暴力递归版本,这题是上一道题的延伸,区别在于上一道题是一个单向数组,而这道题是环形数组,首尾相接。即选择偷第一家就不能偷最后一家,反之亦然。
  • 那么我们可以参考上一道题的解法,将这道题拆分成两种情况,去掉最后一家的最大金额和去掉第一家的最大金额,两者相比就能得出最大金额。
    代码参考上一道题,将数组换一下范围即可,并无大差别。

错排问题

具体可以看彻底搞懂错排公式

问题:现有10本书按照顺序摆放,现要求重新排列,使得新的书的顺序中每一本书都不在原来的位置,求有多少种排列方式?

n个元素的错排数记为D(n)。假设n本书一开始是正确放置的,抽出一本k,可以放的选择有(n-1)个,假设放在了第m个位置,这时候书m就出来了,它可以有两个选择

  • 放到k那里,其实就是两者互换,这时候的问题就变成了D(n-2)的问题了,所以错排有(n-1)*D(n-2)种
  • 不放k那里,剩下n-1本书,我们在看看错排的定义:每本书都不能呆在某一个特定位置,那么就是这剩下的n-1本进行错排,即D(n-1),所以错排有D(n-1)*(n-1)种

结论:D(n)=D(n-1)*(n-1)+D(n-2)*(n-1)


母牛生产(不死神牛)

题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。

假设f(n)为当下母牛的数量,根据母牛三年一生的规律,当下母牛的数量等于前一年母牛的数量f(n-1)加上三年前出生的母牛生下来的小牛的数量f(n-3)。

结论:dp[i]=dp[i-1]+dp[i-3]

变形:假设母牛只有十年寿命,那么减去十年前出生的母牛数量即可


矩阵路径

64. 最小路径和(Medium)

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

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

示例:

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

题解:一开始想到的就是暴力递归的方法来做,直接比较往下走和往右走的路径和,选个小的加上当前值即可

class Solution {
    public int minPathSum(int[][] grid) {
        return calMinPath(grid, 0, 0);
    }

    //i,j表示坐标
    private int calMinPath(int[][] grid, int i, int j) {
        //走到右下角就停止(矩阵只有一个点的话就是它本身的值)
        if (i == grid.length - 1 && j == grid[0].length - 1){
            return grid[i][j];
        }
        //走到最下面一行,只能向右
        if (i == grid.length - 1){
            return grid[i][j] + calMinPath(grid, i, j + 1);
        }
        //走到最右一列,只能往下
        if (j == grid[0].length - 1){
            return grid[i][j] + calMinPath(grid, i + 1, j);
        }
        
        //一般情况,选择小的那条路走
        return grid[i][j] + Math.min(calMinPath(grid, i + 1, j), calMinPath(grid, i, j + 1));
    }
}

解法二:转成动态规划,自变量为两个坐标,因变量为路径和,不变的数组,因此可以画出一个二维图,到达最后一行和最后一列的时候走的方向是固定的,所以可以先填充最后一行和最后一列,然后再填充一般情况的。

dp[]含义:当前点到右下角的最小路径和

public class Solution {
    public int minPathSum(int[][] grid) {
        if (grid == null || grid.length == 0 || grid[0] == null || grid[0].length == 0) {
            return 0;
        }

        int row = grid.length;
        int col = grid[0].length;
        int[][] dp = new int[row][col];

        dp[row - 1][col - 1] = grid[row - 1][col - 1]; //右下角格子是固定的
        
        //填充最后一行
        for (int j = col - 2; j >=0 ; j--) {
            //当前dp值等于右边的dp值加上当前值
            dp[row - 1][j] = dp[row - 1][j + 1] + grid[row - 1][j];
        }
        
        //填充最后一列
        for (int i = row - 2; i >= 0; i--) {
            //当前dp值等于下边的dp值加上当前值
            dp[i][col - 1] = dp[i + 1][col - 1] + grid[i][col - 1]; 
        }

        //填充一般情况
        for (int i = row - 2; i >= 0; i--) {
            for (int j = col - 2; j >= 0; j--) {
                dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) + grid[i][j];
            }
        }
        return dp[0][0];
    }
}

62. 不同路径(Medium)

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

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

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

说明:m 和 n 的值均不超过 100。

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

示例2:

输入: m = 7, n = 3
输出: 28

题解:这道题和上道题类似,不同的是上道题求最小路径和,这道题求的是路径数,可以想到使用dfs来解决,每个位置都可以选择向右或者向下走(两个边界除外),到达右下角的时候,sum加一。直接上动态规划版本。

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[n][m];
        dp[0][0] = 1;
        for (int i = 1; i < m; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < n; i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < m; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[n - 1][m - 1];
    }
}

解法二:空间优化版本:dp[j] = dp[j] + dp[j - 1],等号右边分别是该位置上边的值和左边的值

public int uniquePaths(int m, int n) {
    int[] dp = new int[n];
    Arrays.fill(dp, 1);
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j] + dp[j - 1];
        }
    }
    return dp[n - 1];
}

数组区间

303. 区域和检索 - 数组不可变(Easy)

给定一个整数数组  nums,求出数组从索引 i 到 j  (i ≤ j) 范围内元素的总和,包含 i,  j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

你可以假设数组不可变。
会多次调用 sumRange 方法。

题解:首先可以想到的是暴力递归,(最粗暴的取一次就遍历一次的复杂度太高就不写)。将所有情况都列举出来,需要什么范围就取,。在i<=j的前提下。因为i<=j,所以画出来的应该是一个等腰三角形

  • 数组dp[i][j]存储从i到j的和,初始化dp[0][0]=nums[0]
  • 第一列:dp[0][j]表示从数组第一个元素加到j元素,满足dp[0][j]=dp[0][j-1]+nums[j]
  • 其他的满足dp[i][j] = dp[i-1][j] - nums[i-1];
  • ,当i=j时,dp[i][j]=nums[i][j],连起来组成等腰三角形的斜边
class NumArray {

    private int[][] dp;
    
    public void NumArray(int[] nums) {
        int n = nums.length;
        //建立dp数组即初始化
        dp = new int[n][n];
        dp[0][0] = nums[0];
        
        //填充dp,等腰直角三角形
        for (int j = 1; j < n; j++) {
            //填充行
            dp[0][j] = dp[0][j - 1] + nums[j];
            //填充dp[0][j]对应的列
            for (int i = 1; i <= j; i++) {
                dp[i][j] = dp[i - 1][j] - nums[i - 1];
            }
        }
    }

    public int sumRange(int i, int j) {
        return dp[i][j];
    }
}

解法二:假设我们预先计算了从数字 0 到 k 的累积和。我们可以看出,需要的那个结果是总的减去前面多余的多余,比如dp[2][5]=dp[0][6]-dp[0][2],式子是sumrange(i,j)=sum[j+1]−sum[i]

public class Solution {

    private int[] sum;

    public void NumArray(int[] nums) {
        sum = new int[nums.length + 1];
        //算出0加到sum-1的和
        for (int i = 0; i < nums.length; i++) {
            sum[i + 1] = sum[i] + nums[i];
        }
    }

    public int sumRange(int i, int j) {
        return sum[j + 1] - sum[i];
    }

}

413. 等差数列划分(Medium)

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

例如,以下数列为等差数列:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下数列不是等差数列。

1, 1, 2, 5, 7

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。 如果满足以下条件,则称子数组(P, Q)为等差数组:

元素 A[P], A[p + 1], ..., A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。 函数要返回数组 A 中所有为等差数组的子数组个数。

示例:

A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。

解法一:最简单直白的方法就是考虑每一对元素(之间至少隔着一个元素),对两个元素之间的所有元素来判断是不是等差数列,从最短的长度为三的开始判断,到整个数组。

class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int count = 0;
        //遍历整个数组(A[i,j])
        for (int i = 0; i < A.length; i++) {
            //差值
            int d = A[i + 1] - A[i];
            //等差数列长度最短为3
            for (int j = i + 2; j < A.length; j++) {
                int k = 0;
                //判断A[i,j]内相邻元素对的差值是否相等
                for (k = 1; k <= j; k++) {
                    if (A[k] - A[k - 1] != d){
                        break;
                    }
                }
                //遍历完了整个区间,说明是等差数组
                if (k > j){
                    count++;
                }
            }
        }
        return count;
    }
}

解法二:通过简单的观察,可以发现上一个方法有一部分是重复计算的,等差数列的定义是所有相邻区间等差,所以当遇到某个区间为非等差数列时,就不用再扩展了。

public class Solution {

    public int numberOfArithmeticSlices(int[] A) {
        int count = 0;
        //遍历整个数组(A[i,j])
        for (int i = 0; i < A.length; i++) {
            //差值
            int d = A[i + 1] - A[i];
            for (int j = i + 2; j < A.length; j++) {
                //前面数组是等差数列,那么加上一个数字之后差值不变,就还是等差数列
                if (A[j] - A[j - 1] == d){
                    count++;
                }else {
                    break;
                }
            }
        }
        return count;
    }
}

解法三:用递归的方法解决,参考官方解答。

  • 定义一个递归方法 slice(A,i) 来求在区间 (k,i)(k,i) 中,而不在区间 (k,j)(k,j) 中等差数列的个数,其中 j < ij<i。每次递归也都会更新 sumsum 值,假设先知道了slice(A,i-1)的值为x,那么新增一个元素slice(A,i)的值为x+1。
  • 新增等差数列的区间为 (0,i), (1,i), ... (i-2,i)(0,i),(1,i),...(i−2,i),这些区间总数为 x+1。
  • 这是因为除了区间 (0,i) 以外,其余的区间如 (1,i), (2,i),...(i-2,i)(1,i),(2,i),...(i−2,i) 这些都可以对应到之前的区间 (0,i-1), (1,i-1),...(i-3,i-1)(0,i−1),(1,i−1),...(i−3,i−1) 上去,其值为 x。
public class Solution {

    int sum = 0;

    public int numberOfArithmeticSlices(int[] A) {
        slices(A, A.length - 1);
        return sum;
    }

    private int slices(int[] A, int i) {
        //base case
        if (i < 2){
            return 0;
        }
        int ap = 0;
        //新增数字还是等差
        if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]){
            ap = 1 + slices(A, i - 1);
            sum += ap;
        } else {
            slices(A, i - 1);
        }
        return ap;
    }
}

解法四:将递归转为动态规划解答,首先分析出自变量为i,因变量为sum,关系式是dp[i] = dp[i - 1] + 1。基本思路和递归一致,只是递归采取的是后推,动态规划采取的是前推。

public class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int[] dp = new int[A.length];
        int sum = 0;
        for (int i = 2; i < dp.length; i++) {
            //等差
            if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) {
                //关系式
                dp[i] = 1 + dp[i - 1];
                sum += dp[i];
            }
        }
        return sum;
    }
}

分割整数

343. 整数拆分(Medium)

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

说明: 你可以假设 n 不小于 2 且不大于 58。

题解一:一下子看不出来动态规划做法,可以先写暴力递归解法,n的拆分情况如下(本图借鉴自LeetCode题解区):

一个数n可以分解成两树相乘的情况:i和(n-i),或者i与dp[n-i],dp[n-i]表示数字 n-i 任意拆分可得到的最大乘积。那么就可以得到表达式:F(n) = max {i * F(n - i)},i = 1,2,...,n - 1

class Solution {
    public int integerBreak(int n) {
        //base case
        if (n == 2) {
            return 1;
        }
        int res = -1;
        for (int i = 1; i <= n - 1; i ++) {
            //递归
            res = Math.max(res, Math.max(i * (n - i), i * integerBreak1(n - i)));
        }
        return res;
    }
}

题解二:上个解法可以用数组存储F(n)的值来降低时间复杂度,因为在计算过程中有大量的重复计算。

// 记忆化搜索-自顶向下
int[] memory;
public int integerBreak(int n) {
    memory = new int[n + 1];
    return integerBreakHelper(n);
}

public int integerBreakHelper(int n) {
    if (n == 2) {
        return 1;
    }
    // 记忆化的核心
    if (memory[n] != 0) {
        // memory的初始值为0,如果它不为0,说明已经计算过了,直接返回即可
        return memory[n];
    }
    int res = -1;
    for (int i = 1; i <= n - 1; i++) {
        res = Math.max(res, Math.max(i * integerBreakHelper(n - i), i * (n - i)));
    }
    memory[n] = res;
    return res;
}

题解三:将递归改为动态规划,明确自变量:i,因变量res,只有一个变量,所以一维数组即可,初始化dp[2]=1。

public int integerBreak(int n) {
    int[] dp = new int[n + 1];
    dp[2] = 1;
    //开始填充dp数组
    for (int i = 3; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            //继续往下拆分
            dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j)));
        }
    }
    return dp[n];
}

279. 完全平方数(Medium)

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

示例 1:

输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.

示例 2:

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

题解:这道题在广度优先遍历的时候出现过,题目求一个数最少能由几个平方数组成,这种求最少几个数,可以联想到跟求最短路径相似,一下子看不出来动态规划解法,那么按照惯例,先写出暴力递归解法,尝试所有可能。

bfs带节点的解法见链接,遍历过程中,准备一个数组来存储哪些节点已经遍历过。广度优先遍历的链接

class Solution {
    int[] memo;

    public int numSquares(int n) {
        memo = new int[n + 1];
        return numSqu(n);
    }

    private int numSqu(int n) {
        //base case,说明已经遍历完了
        if (memo[n] != 0){
            return memo[n];
        }
        //取得n的平方根
        int val = (int)Math.sqrt(n);
        //如果平方根的平方等于n,直接返回1
        if (val * val == n){
            return memo[n] = 1;
        }

        int res = Integer.MAX_VALUE;
        //遍历i从1到n-1的情况
        for (int i = 1; i * i < n; i++) {
            //递归一次,step就加1
            res = Math.min(res, numSqu(n - i * i) + 1);
        }

        return memo[n] = res;
    }
}

解法二:改成动态规划,根据上述的暴力递归解法,可以得出转移方程:dp[i]=Math.min(dp[i], dp[i-j*j]+1)

public int numSquares(int n) {
        
        //dp[n]表示n的最小平方和的个数
        int[] dp = new int[n + 1];
        
        //初始化
        for (int i = 0; i <= n + 1; i++) {
            dp[i] = i;
        }

        //填充dp数组
        for (int i = 3; i <= n; i++) {
            //求出dp[i]的最小值
            for (int j = 0; j * j <= i; j++) {
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }
        return dp[n];
    }

91. 解码方法(Medium)

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

'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:

输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。

示例 2:

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

题解一:暴力递归(DFS)。

  • 从前往后遍历,如果一开始为0,那么直接返回0
  • 遇到一个数字,有两种划分方法,例如23333,可以划分为23333和23333(前提是两位数小于等于26),第一种记为dp1,第二种记为dp2,那么dp=dp1+dp2。照此可以写暴力递归解法
class Solution {
    public int numDecodings(String s) {
        return getAns(s, 0);
    }

    /**
     *
     * @param s
     * @param start:到达第几个字符
     * @return
     */
    private int getAns(String s, int start) {
        //当划分到最后一个字符时,说明划分成功,加一
        if (start == s.length()){
            return 1;
        }
        //开头是0的不能解码,直接返回0
        if (s.charAt(0) == '0'){
            return 0;
        }

        //第一种划分方式
        int ans1 = getAns(s, start + 1);
        int ans2 = 0;
        
        //判断前两个数字是否小于等于26
        if (start < s.length() - 1){
            int ten = (s.charAt(start) - '0') * 10;
            int one = s.charAt(start + 1) - '0';
            if (ten + one <= 26){
                ans2 = getAns(s, start + 2);
            }
        }
        return ans1 + ans2;
    }
}

题解二:根据暴力递归转化为动态规划,自变量是start,因变量是解码种数,转移方程是dp[i]=dp[i+1]+dp[i+2]。

dp[i] 代表字符串 s[i, s.len-1],也就是 s 从 i 开始到结尾的字符串的解码方式。

public class Solution {

    public int numDecodings(String s) {
        //开头为0,直接返回0
        if (s.charAt(0) == '0'){
            return 0;
        }

        //初始化dp
        int n = s.length();
        int[] dp = new int[n + 1];
        dp[n] = 1;
        if (s.charAt(n - 1) != '0'){
            dp[n - 1] = 1;
        }

        //填充dp数组,从后往前
        for (int i = n - 2; i >= 0; i--) {
            //如果遇到'0',直接跳过
            if (s.charAt(i) == '0'){
                continue;
            }
            int ans1 = dp[i + 1];
            int ans2 = 0;
            int ten = (s.charAt(i) - '0') * 10;
            int one = s.charAt(i + 1) - '0';
            if (ten + one <= 26) {
                ans2 = dp[i + 2];
            }
            dp[i] = ans1 + ans2;
        }
        return dp[0];
    }
}

这道题还可以优化空间,由转移方程可以知道,dp[i]只与dp[i+1]和dp[i+2]有关,所以不需要一个数组,只用三个变量或者两个变量即可解决,这道题本质上和青蛙跳台阶那道题一样,也就是斐波那契数列。具体可以看剑指offer的跳台阶