leetcode刷题——动态规划(1)

继续跟着代码随想录刷题

动态规划五步曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

509. 斐波那契数

力扣题目链接(opens new window)

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

示例 1:

  • 输入:2
  • 输出:1
  • 解释:F(2) = F(1) + F(0) = 1 + 0 = 1
class Solution {
    public int fib(int n) {
        int[] dp=new int[n+1];
        if(n<=1) return n;
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
        
    }
}

70. 爬楼梯

力扣题目链接(opens new window)

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

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

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

示例 1:

  • 输入: 2
  • 输出: 2
  • 解释: 有两种方法可以爬到楼顶。
    • 1 阶 + 1 阶
    • 2 阶
class Solution {
    public int climbStairs(int n) {
        int[] dp=new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
        
    }
}

746. 使用最小花费爬楼梯

力扣题目链接(opens new window)

旧题目描述

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

  • 输入:cost = [10, 15, 20]
  • 输出:15
  • 解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        // dp[i]表示爬到第i个阶梯是花费是dp[i];
        int[] dp=new int[cost.length+1];
        // 因为可以直接从0或者1开始,那就是免费到达了
        dp[0]=0;
        dp[1]=0;
        // 当前阶梯i等于(i-1阶梯走一步的花费)和(i-2走2步的花费)之间的最小值
        for(int i=2;i<=cost.length;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[cost.length];
        
    }
}

62.不同路径

力扣题目链接(opens new window)

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

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp=new int[m][n];
        for(int i=0;i<m;i++){
           dp[i][0]=1;
        }
        for(int j=0;j<n;j++){
                dp[0][j]=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

力扣题目链接(opens new window)

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

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

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

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int row = obstacleGrid.length;
        int col = obstacleGrid[0].length;
        // int数组默认值为0
        int[][] dp = new int[row][col];
        for (int i = 0; i < col; i++) {
            if (obstacleGrid[0][i] == 1)
                break;
            dp[0][i] = 1;

        }
        for (int i = 0; i < row; i++) {
            if (obstacleGrid[i][0] == 1)
                break;
            dp[i][0] = 1;

        }
        for (int i = 1; i < row; i++) {
            for (int j = 1; j < col; j++) {
                if (obstacleGrid[i][j] == 0)
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[row - 1][col - 1];

    }
}

343. 整数拆分

力扣题目链接(opens new window)

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

示例 1:

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

示例 2:

  • 输入: 10
  • 输出: 36
  • 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
  • 说明: 你可以假设 n 不小于 2 且不大于 58。
class Solution {
    public int integerBreak(int n) {
        int[] dp=new int[n+1];
        dp[2]=1;
        // j*(i-j)代表拆分成2个整数 有时候不拆的值反而更大 ,例如dp[3]=2比3小 
        // j*dp[i-j]代表拆分3个及3个以上的整数,因为dp[i-j]表示至少拆分成2个整数
        // 不能是dp[j]*dp[i-j] 因为这个代表拆分成至少4个整数
        for(int i=3;i<=n;i++){
            for(int j=1;j<=i/2;j++){
                dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
        
    }
}

96.不同的二叉搜索树

力扣题目链接(opens new window)

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

class Solution {
    public int numTrees(int n) {
        // dp[i] : i个不同节点构成的二叉搜索树的个数为dp[i]。不一定说是从1-i 可以是2-3-4 总共3个有序节点
        // dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
        // 元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
        // 元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
        // 元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
        // 头节点为1代表剩下的2-3只能是作为1的右子树的2个节点,左节点为0个
        // 节点为2代表左子树都是小于2的那只能是1,右子树为大于2的只能是3
        // dp[i]表示i个节点的不同二叉搜索树个数,所以比如2-3-4总3个节点的排序数量也可以表示为dp[3]
        // 递推公式:把j作为头节点,从1-i进行遍历,dp[i] += dp[j - 1] * dp[i - j]; 
        // 如果j为节点,那左子树的节点数量就是1-(j-1) 总共j-1个,所以表示为dp[j-1];
        // 那右子树的节点数量就是(j+1)-i 总共i-j个,所以表示为dp[i-j];
        int[] dp=new int[n+1];
        if(n==1) return 1;
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            for(int j=1;j<=i;j++){
                // 注意这里是乘法,是算数量,比如左边2种和右边3种组合,那就是2*3=6种组合
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
                
    }
}

01背包理论基础

01 背包

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

二维dp数组01背包

dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。

递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

  • 不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。

  • 放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

dp数组如何初始化

//第一行的初始化
for (int j = 0 ; j < weight[0]; j++) {  
// 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略。
    dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
//第一列的初始化全为0 因为背包容量为0
//dp[j][0]=0;

一维dp数组(滚动数组)

如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  • 不放物品i:dp[j]
  • 放物品i:指定是取最大的,毕竟是求最大价值,dp[j - weight[i]] + value[i]

dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。

一维dp数组遍历顺序

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

    }
}

逻辑就是先把物品0分别放入重量0-j的背包,dp[j]可以选择放或者不放物品0

然后再继续放物品1 ,都是倒序

倒序遍历是为了保证物品i只被放入一次!

一维dp还是有点难以理解

举例说明:比如物品1,遍历背包,重量j为4,dp[4]=max(dp[4],dp[4-3]+value[1])

这里就用到了dp[4-3],是上一层物品0遍历后得到的dp[1] ,表示还没放入物品1

如果正序遍历,那dp[1]已经发生变化,已经放入了物品1,就错误了

dp[i] 需要用到的dp[  j-weight[i]  ]必须是还没处理过物品 i 的dp[  j-weight[i]  ]

所以我们需要倒序遍历,不能正序,如果正序,那dp[  j-weight[i]  ]已经处理过物品i了,不符合

416. 分割等和子集

力扣题目链接(opens new window)

题目难易:中等

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意: 每个数组中的元素不会超过 100    数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]
  • 输出: true
  • 解释: 数组可以分割成 [1, 5, 5] 和 [11]
class Solution {
    public boolean canPartition(int[] nums) {
        // 背包的体积为sum / 2
        // 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
        // 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
        // 背包中每一个元素是不可重复放入。
        // dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        if (sum % 2 != 0)
            return false;
        int[] dp = new int[sum / 2 + 1];
        dp[0] = 0;
        for (int i = 0; i < nums.length; i++) {
            // 这里控制条件j >= nums[i]; dp[j - nums[i]]
            for (int j = sum / 2; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }
        return dp[sum / 2] == sum / 2;
    }
}

1049.最后一块石头的重量II

力扣题目链接(opens new window)

题目难度:中等

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

这道题目和上一个题目类似,都是找出数组中 和 接近sum/2的子集

class Solution {
    public int lastStoneWeightII(int[] stones) {
        // 尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了
        int sum = 0;
        for (int i = 0; i < stones.length; i++) {
            sum += stones[i];
        }
        
        int[] dp = new int[sum / 2 + 1];
        dp[0] = 0;
        for (int i = 0; i < stones.length; i++) {
           
            for (int j = sum / 2; j >= stones[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum-2*dp[sum/2];
    }
}

494.目标和

力扣题目链接(opens new window)

难度:中等

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // 先数学推导 left-right=target   left+right=sum  这里的left、right就是数组nums分裂的2个子集的和
        // 推出left=(sum+target)/2
        // 定义dp[j]为凑满和为j的背包的最多的方法
        // 递归:dp[j]=放物品i的方法+不放物品i的方法
        // 不放物品:dp[j]=dp[j] 保持原来的,重量没有变化,注意这里的dp[j]是原来就有值的,可能是0或者大于0,代表前面i-1个物品遍历过后的dp[j]
        // 放物品:dp[j]=dp[j-nums[i]] 放了i物品后,那就看dp[j-nums[i]]有多少种方法
        // 初始化:dp[0]=1;重量为0就不放任何物品咯,就1种方法
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        int left=(sum+target)/2;
        if((sum+target)%2!=0) return 0;       
        if(Math.abs(target)>sum) return 0;
        int dp[] =new int[left+1];
        dp[0]=1;
         for(int i=0;i<nums.length;i++){
            for(int j=left;j>=nums[i];j--){
                dp[j]=dp[j]+dp[j-nums[i]];
            }
            
        }
        return dp[left];
    }
}

474.一和零

力扣题目链接(opens new window)

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3

  • 输出:4

  • 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // dp[m][n]满足最多包含m个1和n个0的最大子集的size
        // 还是0-1背包,用一维dp数组,m和n两个维度当作是全部重量要求
        // 递推公式:取物品i 1+dp[m-nums[i][0]][n-nums[i][1]]
        // 不取物品dp[m][n]
        // 初始化:都是0
        int[][] dp = new int[m + 1][n + 1];
        for (String str : strs) {
            int zeroNum = 0;
            int oneNum = 0;
            char[] ss = str.toCharArray();
            for (char c : ss) {
                if (c == '0')
                    zeroNum++;
                else
                    oneNum++;
            }
            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = Math.max(dp[i][j], 1 + dp[i - zeroNum][j - oneNum]);
                }
            }
        }
        return dp[m][n];
    }
}

完全背包理论基础

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

01背包和完全背包唯一不同就是体现在遍历顺序上

01背包的核心代码

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历

// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!

这里说的是纯完全背包问题,其for循环的先后循环是可以颠倒的!

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

// 先遍历背包,再遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}

518.零钱兑换II

力扣题目链接(opens new window)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额

class Solution {
    public int change(int amount, int[] coins) {
        // dp[j]表示凑成金额j的最多组合方式
        // 递推公式:dp[j]=不取物品i (dp[j])+取物品i(dp[j-coins[i]])
        // 初始化dp[0]=1; 凑成金额0就1种方式
        int[] dp=new int[amount+1];
        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]];
        }
        // coins[0]=2,coins[1]=3
        // 先遍历物品再背包求得是组合 每个背包先一个一个的放物品coins[0] 再放coins[1] 所以不会有倒序情况
        // 先背包再物品求的是排列,每个背包都会处理coins[0]和coins[1] 有顺序(2,3)(3,2)
        //排列: dp[5]+=dp[5-2]——放了3再放2,dp[3]先遍历后再到dp[5],所以dp[3]已经放了3; 
        //  dp[5]+=dp[5-3](放了2再放3);dp[2]先遍历后再到dp[5],所以dp[2]已经放了2;
        // 所以先背包再物品就会导致排列
        return dp[amount];
    }
}

总结:

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

377. 组合总和 Ⅳ

力扣题目链接(opens new window)

难度:中等

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

class Solution {
//这道题目就是完全背包的排列问题,先遍历背包再物品
    public int combinationSum4(int[] nums, int target) {
        int[] dp=new int[target+1];
        dp[0]=1;
        for(int j=0;j<=target;j++){
            for(int i=0;i<nums.length;i++){
                if(j>=nums[i]) dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[target];
        
    }
}

322. 零钱兑换

力扣题目链接(opens new window)

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

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

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1
class Solution {
    public int coinChange(int[] coins, int amount) {
        // 完全背包 一个物品有无限个
        // dp[j]代表凑满和为j的最小数量
        // 递推公式:(1)取物品i dp[j]=1+dp[j-nums[i]]
        // (2)不取物品i dp[j]=dp[j] dp[j]=Math.min(dp[j],1+dp[j-nums[i]]);
        // 初始化dp[0]=0 凑满和为0的最小数量为0,是可以凑够的,只不过数量为0
        // 这里注意还要根据递推公式初始化其他下标的值,dp[j]必须初始化为一个最大的数,
        // 否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中【被初始值覆盖】。
        // 遍历顺序:组合或者排列可以,只需要数量,所以哪个遍历顺序都行
        int[] dp=new int[amount+1];
        dp[0]=0;
         for(int j=1;j<=amount;j++){
            // 初始值代表没有方法凑够重量
            dp[j]=Integer.MAX_VALUE;
         }
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++)
            // 这一步需要非常注意 如果遇到初始值那就跳过 初始值代表没有方法凑够重量
                   if(dp[j-coins[i]]!=Integer.MAX_VALUE)
                        dp[j]=Math.min(dp[j],1+dp[j-coins[i]]);
                
        }
        if(dp[amount]==Integer.MAX_VALUE) return -1;
        return dp[amount];
        
    }
}

279.完全平方数

力扣题目链接(opens new window)

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

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

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

示例 1:

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

这个题目和上一个相似,就是物品不是从数组获取,而是变成 i*i

class Solution {
    public int numSquares(int n) {
        // 1 4 9 16 25 36 。。。物品
        // 重量j=n
        // dp[j]凑满和为j的多个平方数的最小数量
        // 递推公式:dp[j]=min(dp[j],1+dp[j-nums[i]])
        // 初始化:dp[0]=0 其他下标需要用MAX_VALUE最大化 避免被递推过程被初始值覆盖
        // 遍历顺序,组合或者排列都可以
        int[] dp = new int[n + 1];
        for (int j = 0; j <= n; j++)
            dp[j] = Integer.MAX_VALUE;
        dp[0] = 0;
        for (int i = 1; i <= 100; i++) {
            for (int j = i * i; j <= n; j++)
                // 这一步不需要,因为完全平方一定可以凑成的 比如1
                // if(dp[j-i*i]!=Integer.MAX_VALUE)
                dp[j] = Math.min(dp[j], 1 + dp[j - i * i]);
        }
        return dp[n];

    }
}

139.单词拆分

力扣题目链接(opens new window)

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

  • 输入: s = "leetcode", wordDict = ["leet", "code"]
  • 输出: true
  • 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 这个题目属于常规的动态规划吧
        // dp[i] : 字符串长度为j的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
        // 递推公式:dp[i]=判断 子字符串(j到i)是否存在字典里面&& 子字符串(0-j)是否为true 条件都满足才可以判断dp[i]=true
        // 比如leetcode 求dp[5]即leetc是否满足可以拆分为一个或多个在字典中出现的单词
        // leetc是否存在&&dp[0](“”)=true eetc是否存在&&dp[1](“l”)=true
        // etc是否存在&&dp[2](“le”)=true tc是否存在&&dp[3](“lee”)=true c是否存在&&dp[4](“leet”)=true
        // 所以这里需要一个变量j从0开始分割(0-i)的字符串,保证分割后的2个子字符串都满足条件
        // 初始化dp[0]=true; 空字符串当然也是存在字典的
        HashSet<String> set = new HashSet(wordDict);
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i; j++) {
                String split = s.substring(j, i);
                // if(find(split,wordDict)&&dp[j]){
                // 这里就是把0-i的字符串分割成0-j和j-i的2个子字符串,判断2个都需要在字典出现,那dp[i]=true
                if (set.contains(split) && dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
    // public boolean find(String s,List<String> wordDict){
    // for(String str:wordDict){
    // if(str.equals(s)) return true;
    // }
    // return false;
    // }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值