继续跟着代码随想录刷题
动态规划五步曲
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
509. 斐波那契数
斐波那契数,通常用 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. 爬楼梯
假设你正在爬楼梯。需要 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. 使用最小花费爬楼梯
旧题目描述:
数组的每个下标作为一个阶梯,第 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.不同路径
一个机器人位于一个 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
一个机器人位于一个 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. 整数拆分
给定一个正整数 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.不同的二叉搜索树
给定一个整数 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. 分割等和子集
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 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
题目难度:中等
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 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.目标和
难度:中等
给定一个非负整数数组,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.一和零
给你一个二进制字符串数组 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
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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. 组合总和 Ⅳ
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
- 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. 零钱兑换
给定不同面额的硬币 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.完全平方数
给定正整数 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.单词拆分
给定一个非空字符串 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;
// }
}