背包问题(01,完全)

贴一个代码随想录的网址:

代码随想录 (programmercarl.com)

1:01背包的概念:

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

完全背包:

        完全背包和01背包的区别就是01背包物品的个数是有限的,完全背包是无限个的。

暴力解法:

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

暴力解法提一下就好:下面登场的是动态规划的解法。

2:具体做法:

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

dp[i][j]中的i和j到底表达什么意思,i:从0~i的物品中挑选,在背包容量为j的时候,最大的价值是多少。

2:确定递推公式:

这个递推公式如何思考呢?

根据我们刚刚说的这个暴力解法,我们会发现:

对于每一个物品来说,我们有两种选择:选或者不选。

如果不选,那我们的背包容量不会减少(j不会减少),同样价值也不会增多,那这个时候就和之前的价值是一样的了:dp[i - 1][j]

如果选,那我们的背包容量会减少,价值也会增多:dp[i - 1][j - weight[i]] + value[i]

所以这个方程就是:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

3:dp数组初始化:

这个数组初始化。

因为我们知道,动态规划讲究一个推导,就是由一个状态推导到另一个状态,那我们应该初始化什么呢?

我们应该初始化最初的状态。

这道题最初的状态是什么呢,我们可以考虑二维数组的第一行和第一列,就是背包的容量为0的时候和只有物品0的时候。

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];
}

4:确定遍历顺序:

这个遍历顺序是如何确定的呢?

首先还是回归到动态规划的思想:由一个状态推导到另一个状态,所以,我们遍历的时候要想保证不会出错,我们得保证推出下一个状态的这一个状态已经算出来了。

是先遍历背包还是先遍历物品呢?

虽然说这道题如果开二维数组的话,那两种方式都是可以的,但是遍历物品更好理解。

import java.util.Arrays;

public class BagProblem {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 初始化 dp 数组做了简化(给物品增加冗余维)。这样初始化dp数组,默认全为0即可。
     * dp[i][j] 表示从下标为[0 - i-1]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
     * 其实是模仿背包重量从 0 开始,背包容量 j 为 0 的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为 0。
     * 可选物品也可以从无开始,也就是没有物品可选,即dp[0][j],这样无论背包容量为多少,背包价值总和一定为 0。
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods + 1][bagSize + 1];  // 给物品增加冗余维,i = 0 表示没有物品可选

        // 初始化dp数组,默认全为0即可
        // 填充dp数组
        for (int i = 1; i <= goods; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i - 1]) {  // i - 1 对应物品 i
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i - 1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]);  // i - 1 对应物品 i
                }
            }
        }

        // 打印dp数组
        for(int[] arr : dp){
            System.out.println(Arrays.toString(arr));
        }
    }
}

3:状态压缩(一维数组):

这道题可以压缩(将二维数组压缩成一维数组)

仔细看我们的状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

dp[i]层的值是从dp[i-1]层推导出来的,我们推导完之后,那我们dp[i-1]层的值还有用嘛,其实就是已经没用了,基于此,我们就可以进行覆盖。

这里有点难理解,我们可以举一个简单的动态规划的题目进行举例,比如

斐波那契函数:

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

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

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

这是一道非常简单的dp题目:

题解:

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

这里的第三个元素是由前两个元素推导而来的,当获取到第三个元素的时候,第一个元素dp[0]就已经没有用了,这个时候我们可以考虑对这个元素进行覆盖。

class Solution {
    public int fib(int n) {
        if (n < 2) {
            return n;
        }
        int p = 0, q = 0, r = 1;
        for (int i = 2; i <= n; ++i) {
            p = q; 
            q = r; 
            r = p + q;
        }
        return r;
    }
}

维护三个变量即可。

回到到这个01背包的题目,也是同样的道理,也可以进行覆盖。

代码随想录 (programmercarl.com) 参考代码随想录的滚动数组。

滚动数组具体做法:

1:确定dp数组含义:

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

2:确定递推公式:

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

3:dp数组初始化: 

这里的dp数组初始化又如何思考呢?

dp[0] 很自然肯定是等于0,因为你这个时候,你背包容量是0,什么都装不了,那价值肯定就是0了。

那其它值如何初始化呢?

如果对初始化值不清楚的话,我们就看递推公式,我们是将dp[j]和weight[i]] + value[i]取最大值,

那我们平时做算法题的时候,我们取最大值,我们是不是应该初始化为最小值啊,就是Integer.MINVALUE,不过这一题是背包,背包的容量不可能为负数,所以,初始化为0就可以了。

4:确定遍历顺序:

 这里的遍历顺序就挺有讲究的了,

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]);

    }
}

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!

二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。

为什么呢?

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

。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

为什么倒序能保证数据正确, 在二维数组中每一层的数值是由上一层的正上方和左上方数据得出的

一维数组模拟二维数组,倒序的话可以保证每一层数据也是由上一层正上方和左上方得出的 

如果是正序的话,左上方数据就会被覆盖掉了

    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWight = 4;
        testWeightBagProblem(weight, value, bagWight);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
        int wLen = weight.length;
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagWeight + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < wLen; i++){
            for (int j = bagWeight; j >= weight[i]; j--){
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        //打印dp数组
        for (int j = 0; j <= bagWeight; j++){
            System.out.print(dp[j] + " ");
        }
    }

例题: 

1  分割等和子集:

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

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 

代码:

   class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        int i,j;
        int len = nums.length;
        for(i=0;i<len;++i){
            sum += nums[i];
        }
        if(sum%2==1) return false;
        int bagsize = sum/2;//确定背包的容量。
        int[] dp = new int[bagsize+1];
        for(i=0;i<len;++i){
            for(j=bagsize;j>=nums[i];j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[bagsize]*2==sum?true:false;
    }
}

 2:最后一块石头的重量II:

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

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

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

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

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        int i,j;
        int len = stones.length;
        for(i=0;i<len;++i){
            sum += stones[i];
        }
        int bagsize = sum/2;//确定背包的容量
        int[] dp = new int[bagsize+1];
        for(i=0;i<len;++i){
            for(j=bagsize;j>=stones[i];j--){
                dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return sum-dp[bagsize]*2;
    }
}

第一题是给我们背包的容量(sum/2)问我们是否能装满?

第二题也是给我们背包的容量(sum/2),让我们尽量去装?

这里的dp数组含义就是背包容量为j时,最大装的价值。

这两道题的递推公式:dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i])

 3:目标和:

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int i,j;
        int sum = 0;
        int len = nums.length;
        for(i=0;i<len;++i){
            sum += nums[i];
        }
        if(Math.abs(target)>sum) return 0;
        if((target+sum)%2!=0) return 0;
        int bagsize = (target+sum)/2;
        int[] dp = new int[bagsize+1];
        dp[0] = 1;
        for(i=0;i<len;++i){
            for(j=bagsize;j>=nums[i];--j){
                dp[j] += dp[j-nums[i]];
            }
        }
        return dp[bagsize];
    }
}
把这个数组分成两个集合,一个集合前面的符号是+,一个是-
假设正数和是x,那负数和就是sum-x,
x - (sum-x) = tar
解得 x = (tar+sum)/2,
所以这个x其实是固定的,我们只需要在这个集合中找到有多少种集合 == x就可以了
转化成背包问题就是:给我们背包的容量,问我们有多少种方式能把背包装满

这里的dp数组含义是:背包容量为j时,有多少种方法能装满这个背包

第三题给我们背包的容量(tar+sum)/2 ,问我们有多少种方式能把背包装满

求多少种方式的递推公式:dp[j] += dp[j-nums[i]];

4:一和零: 

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

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

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

输入: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 。

第四道题还挺难,说实话第一眼看过去真挺难,还被吓到了,其实本质上也是一个背包问题,只不过这个背包有两个维度:m和n

dp数组的含义:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]

递推公式:dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1)

这个数组中的每个数的weight不是直接给我们的,像最典型的背包问题,他就会直接告诉你这个物品的weight,前几题weight和value都是这个数字本身,

这道题的weight是这个String的0和1的个数。

价值是1,因为我们统计的是这个字串的个数,所以,无论多长的String,都是1。

分析到这,这道题就和01背包一模一样了。

01背包总结: 

1:背包问题的难点不是在于这个状态转移方程,难得是初始化和确定遍历顺利这两步。

2:背包问题的滚动数组的方式是由二维数组压缩而来的,碰到题目可以先尝试把二维数组的代码写出来。

3:至此,01背包的题目算是刷的差不多了,我感觉能用01背包解决的题目都有一个共同点:

就是要分堆 ,题目的不同就是分堆规则的不同,和我们要求的东西的不同而有


来不及休息了,接下来要登场的是

完全背包:

完全背包的概念:

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

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

直接上代码把,我感觉这个完全背包问题和之前的01背包差不多,唯一的区别就是可以重复,

想想在01背包中如何保证每个物品只选一次,我们是不是将背包的容量从后往前遍历,

那完全背包就是从前往后遍历:

private static void testCompletePack(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++){ // 遍历物品
        for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

例题:

1:零钱兑换II:

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

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

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

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

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
class Solution {
    public int change(int amount, int[] coins) {
        int i,j;
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for(i=0;i<coins.length;++i){
            for(j=coins[i];j<=amount;++j){
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
}

这道题其实没什么难得,首先,我们知道这个零钱,我们是可以任意取得,所以这就是一个完全背包问题,在物品中找东西填满背包,

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

递归公式:因为之前做过01背包得题目,求有多少种方式这种问题都是:

dp[j] += dp[j - coins[i]]

 2:组合总和IⅤ:

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int i,j;
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(i=0;i<=target;++i){
            for(j=0;j<nums.length;++j){
                if (i >= nums[j]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
}

这题也是一个完全背包问题,和上一题得不同就是这一题是求一个排列,上一题是求组合。

说到这个组合和排列得话,有需要重新复习一下这个组合和排列得知识了

组合是不强调元素顺序的,排列是强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。

记住组合无序,排列有序,就可以了

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

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

如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!

所以本题遍历顺序最终遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历

3:零钱兑换:

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

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

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

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
class Solution {
    public int coinChange(int[] coins, int amount) {
        int i,j;
        int[] dp = new int[amount+1];
        for(i=0;i<amount+1;++i){
            dp[i] = Integer.MAX_VALUE;
        }
        //dp[i]表示从0~i需要得硬币个数
        dp[0] = 0;
        for(i=0;i<coins.length;++i){
            for(j=coins[i];j<=amount;++j){
                 if (dp[j - coins[i]] != Integer.MAX_VALUE) {
                     //选择硬币数目最小的情况
                     dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount]==(Integer.MAX_VALUE)?-1:dp[amount];
    }
}

本题从和上面两个题分别对比:

和零钱兑换Ⅱ对比:

这题得零钱兑换和零钱兑换Ⅱ对比,所求得东西不一样,这一题是求最少能凑成总金额得最少硬币数,零钱兑换Ⅱ是求有多少种方式能凑成总金额,那具体的差别体现就在递推公式上的不同。

零钱兑换Ⅱ:dp[i] += dp[i - nums[j]];

零钱兑换:dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);

和组合总和Ⅳ对比:

本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数

所以本题并不强调集合是组合还是排列。

所以遍历顺序是先物品再背包。

4:完全平方数: 

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

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

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

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

class Solution {
    public int numSquares(int n) {
        double result = Math.sqrt(n);
        int[] bag = new int[(int)result+1];
        int i,j;
        for(i=1;i<=result;++i){
            bag[i] = i*i;
        }
        int[] dp = new int[n+1];//背包大小
        for(i=1;i<=n;++i){
            dp[i] = Integer.MAX_VALUE;
        }
        dp[0] = 0;
        for(i=1;i<=result;i++){//外层遍历物品
            for(j=bag[i];j<=n;++j){//内层遍历背包容量
                if(dp[j-bag[i]]!=Integer.MAX_VALUE){
                    dp[j] = Math.min(dp[j],dp[j-bag[i]]+1);
                }
            }
        }
        return dp[n];
    }
}

做了前面好多的背包问题之后,这道题其实就很容易想到了。

背包大小就是题目给我们的这个数字,

我们要用完全平方数去凑成这个数字,那我们的物品很自然的就能想到就是这个完全平方数。 

还有一个小小细节就是:因为我们要求最少的完全平方数,所以我们初始化这个dp数组的时候,可以初始化为最大值:Integer.MAX_VALUE

 5:单词拆分:

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> record = new HashSet<>(wordDict);
        boolean[] dp = new boolean[s.length()+1];
        dp[0] = true;
        int i,j;
        for(i=1;i<=s.length();++i){//先遍历背包
            for(j=0;j<i;++j){//再遍历物品
                String word = s.substring(j,i);
                boolean flag = record.contains(word);
                if (flag&&dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

 这道题我觉得还蛮难的,主要的难点体现在:1:递推公式有点难想到,2:遍历的顺序。

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

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词

确定递推公式:

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true(j < i)。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

遍历顺序:

这道题一定是先遍历背包再遍历物品,为什么呢?

对比第二题组合总和的话,这样的遍历顺序是求排列的方式。

那为什么要求排列呢?

拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。

"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。

"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。

所以说,本题一定是 先遍历 背包,再遍历物品。

小细节:

1:通过HashSet来更快在数组种查找我们想要的单词是否在这个数组中。

2:我们保证两个条件都成立的情况下再去更新dp数组种的值?

什么意思呢?

我原来写的代码是这样的:

        for(i=1;i<=s.length();++i){//先遍历背包
            for(j=0;j<i;++j){//再遍历物品
                String word = s.substring(j,i);
                boolean flag = record.contains(word);
                dp[i] = flag&&dp[j];
            }
        }

 就是我直接就去更新了,这就会导致,到最后我的dp数组全是false。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值