动态规划问题总结

首先介绍一下01背包和完全背包:

01背包:

这里只讨论滚动数组的情况,01背包需要先遍历物品,再遍历容量,并且容量倒序遍历,如果遍历背包容量放在外层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。

容量需要倒序遍历的原因:因为是01背包,物品只能放入一次,顺序遍历的话物品会放入多次,就变成完全背包了

 

 先遍历物品,再遍历容量:

 先遍历容量,再遍历物品:

可以看到,容量为5的时候依赖于容量为4,3,2,1时候的结果,但是由于容量是倒序遍历的,容量为4,3,2,1的结果并没有出来,所以容量为5的时候只放入了一个物品

完全背包:

完全背包和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循环嵌套顺序是无所谓的!

物品在外层,背包容量在内层:

背包容量在外层,物品在内层:

因为背包容量是顺序遍历,遍历到dp[3]的时候,dp[2]和dp[1]的结果全部出来了,所以不影响。

一般背包问题有五类:

1、组合与排列组合

2、最多能放入多少值

3、装满背包有几种方法

4、背包装满最大价值(二维)

5、装满背包所有物品的最小个数

1.1.组合问题:

        比如leetcode的第518题

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

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

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

问题特征:简而言之,就是给你一个数组num,里面装的是数字或者字符,给你一个数targe,可以是数字或者字符,计算出数组中能凑出值为target的个数

解析:看到个数,就是经典的组合问题,并且通过假设每一种面额的硬币有无限个这个信息,我们要使用完全背包而不是01背包来解决。

模板:如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。

例如:dp[j](dp[j]:凑成容量为j有多少种方法),j 为5

  • 已经有一个值为1的硬币的(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个值为2的硬币的(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个值为3的硬币的(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个值为4的硬币的(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个值为5的硬币的 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

则dp[j] = dp[1] + dp[2] + dp[3] + dp[4] + dp[5]

public int template1(int[] nums, int target)
{
    int[] dp = new int[target+1];
    int n = nums.length;
    dp[0] = 1;
    for (int i = 0; i < n; i++)
    {
        for (int j = nums[i];j <= target;j++)
        {
            dp[j] += dp[j-nums[i]];
        }
    }
    return dp[target];
}

 1.2 排列组合问题

        比如leetcode的第377题

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

示例: 

输入: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)
请注意,顺序不同的序列被视作不同的组合。

解析:这里我们可以看出和组合问题有一个很明显的不同点, 顺序不同的序列被视作不同的组合,这就是组合排列问题,与元素组合的顺序相关

模板:

public int combinationSum4(int[] nums, int target)
{
    Arrays.sort(nums);
    int[] dp = new int[target+1];
    dp[0] = 1;
    int n = nums.length;
    // 先遍历背包容量
    for (int j = 1; j <= target; j++)
    {
        // 再遍历数组元素
        for (int i = 0;i < n; i++)
        {
            if (nums[i] <= j)
            {
                dp[j] += dp[j-nums[i]];
            }
        }
    }
    return dp[target];
}

这里为什么先遍历背包容量,再遍历数组元素就能解决排列的问题呢

遍历顺序:如果按照往常这样计算的话,nums放在外循环,target在内循环
举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合
为什么:假如nums = [1,2,3], target = 4,dp[0] = 1
1:遍历nums(物品)放在外循环,遍历target的作为内循环
  nums=1: dp[1] = 1,dp[2] = 1,dp[3] = 1,dp[4] = 1
  nums=2: dp[2] = 2,dp[3] = 2,dp[4] = 3
  nums=3: dp[3] = 3,dp[4] = 4

2:遍历target放在外循环,遍历nums(物品)作为内循环
  j=1: dp[1] = 1
  j=2: dp[2] = 1,dp[2] = 2
  j=3: dp[3] = 2, dp[3] = 3,  dp[3] = 4
  j=4: dp[4] = 4,dp[4] = 6,dp[4] = 7
发现,在j=3,nums=2的时候,情况1的dp[3] = 2,情况2的dp[3] = 3

我们来看情况1:
nums=1,j=3时,dp[3] = dp[3] + (dp[2] + 1这种情况,为1)=> dp[3] = 0 + 1不对
nums=2,j=3时,dp[3] = dp[3] + (dp[1] + 2这种情况,为1)
nums=3,j=3时,dp[3] = dp[3] + (dp[0] + 3这种情况,为1)

说明dp[2] 这里不应该等于1,应该等于2才对,这里dp[2] = 单选2,选两次1两种情况,
但是没考虑到单选2,因为nums=1,所以dp[3]少算了一种
但是如果遍历nums(物品)作为内循环,j=2的时候,dp[2] = 2,下一次j=3的时候,dp[2]成为了最终结果。
dp[3]用dp[2]的最终结果进行计算肯定是没问题的
总结,情况1的dp[3] += (dp[2]+1)这种情况的dp[2]不正确,没有用到dp[2]的最终结果,缺少了dp[2] += 单选2这种情况,说明上一层的dp[3]只有1 ,1 ,1这种情况,导致最后dp[4]结果不对

 

2、最多能放入多少值问题

        比如leetcode的第416题

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

 示例1:

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

 解析:

首先,本题要求集合nums里面能否出现总和为 sum / 2 的子集。也就是求集合nums里,背包容量sum / 2 ,nums中的元素最多能放入的值的总和。

如果dp[sum/2] == sum/2 说明,集合中的⼦集总和正好可以凑成sum/2,理解这⼀点很重要。

1.可以确定dp[i]的含义:容量为i时,nums中的元素最多能放入的值的总和,则i >= dp[i]

注意:物品i相当于元素i在nums数组的下标,value[i]=nums[i],weight[i]= nums[i]

3.递推公式变为dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])

4.dp[i]数组初始化:dp[0] = 0,容量为0,什么都不能放,总和为0

5.遍历顺序问题,如果使用二维数组,当前行的值依赖于上一行的值,可以随意调整遍历顺序

如果使用一维数组,就需要物品顺序遍历,容量倒序遍历

倒序遍历是为了保证物品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 boolean canPartition(int[] nums)
    {
        int n = nums.length;
        int target = 0;
        for (int i = 0; i < n; i++)
        {
            target += nums[i];
        }
        if (target % 2 != 0)
        {
            return false;
        }else
        {
            target = target/2;
        }
        int[] dp = new int[target+1];
        // 先遍历物品,也就是数组的值
        for (int i = 0; i < n; i++)
        {
            // 再遍历背包容量,如果容量小于该物品重量,就不用遍历了
            for (int j = target; j >= nums[i]; j--)
            {
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
        }
        return dp[target] == target;
    }

3、装满背包有几种方法

dp[j] += dp[j - nums[i]],与1.1.组合问题相同,记住递推公式和遍历顺序即可

4、背包装满最大价值

        比如leetcode的第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 。

示例 2: 输入:strs = ["10", "0", "1"], m = 1, n = 1 输出:2 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

 解析:与最多能放入多少值类似,递推公式都是

dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

只不过nums[i]既是容量也是价值,如果nums[i]不对应,价值和容量,就演变成

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

 但是物品价值可能不是一维的,题目可能演变成 两个不同类别的物品装满背包1容量为A,背包2容量为B的最大价值是多少,这时递推公式就演变成

dp[j][k] = max(dp[j][k],dp[j-weight[A]][k-weight[B]]+value[A]+value[B]) 

// 1.确定dp[j][k]的含义:最多有个j个0和k个1的strs的最大子集的大小为dp[j][k]
// 2.确定递推公式:
//      dp[j][k] = max(dp[j][k],dp[j-当前字符串0的个数][k-当前字符串1的个数]+1)
// 3.dp[i]数组初始化:dp[0][0] = 0
// 4.确定遍历顺序:后向前遍历
public int findMaxForm1(String[] strs, int m, int n)
{
    int[][] dp = new int[m+1][n+1];
    int len = strs.length;
    // 遍历物品
    for (int i = 0; i < len; i++)
    {
        // 遍历容量,01背包,从后往前遍历
        int zeroNum = searchZeroAndOneNum(strs[i])[0];
        int oneNum = searchZeroAndOneNum(strs[i])[1];
        for (int j = m; j >= zeroNum;j--)
        {
            for (int k = n; k >= oneNum;k--)
            {
                // 这里把物品装进背包的价值就是1 
                dp[j][k] = Math.max(dp[j][k],dp[j-zeroNum][k-oneNum]+1);
            }
        }
    }
    return dp[m][n];
}

5、装满背包所有物品的最小个数

        比如leetcode的322题:

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

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

示例 1: 输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1

示例 2: 输入:coins = [2], amount = 3 输出:-1

示例 3: 输入:coins = [1], amount = 0 输出:0

示例 4: 输入:coins = [1], amount = 1 输出:1

示例 5: 输入:coins = [1], amount = 2 输出:2

与 背包装满最大价值问题类似,只是变成了最小,dp数组的含义相应变化就可以了

// dp[j]: 在0-i这个硬币数组的范围内,凑足总额为j所需钱币的最少个数为dp[j]
// 递推公式:dp[j] = min(dp[j],dp[j-coins[i]]+1)
// 该层最小个数为上一层的最小个数,与
// 加入coins[i]这个硬币之后,dp[j-coins[i]]就是没加这个硬币,但是剩余容量刚好是coins[i],能凑成j-coins[i]的最小个数,
// 返回这两者的最小值即可
public int coinChange(int[] coins, int amount)
{
    int[] dp = new int[amount+1];
    Arrays.fill(dp,Integer.MAX_VALUE);
    dp[0] = 0;
    int n = coins.length;
    for (int i = 0; i < n; i++)
    {
        for (int j = coins[i];j <= amount;j++)
        {
            // 如果dp[j-coins[i]] = Integer.MAX_VALUE,说明j-coins[i]是无法凑成的,
            // j是无法通过dp[j-coins[i]] + coins[i]凑成的
            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] ;
}

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
01背包问题动态规划中的一个经典问题,它的解法也非常经典,下面是我对该问题动态规划总结。 1. 状态定义 定义一个二维数组dp[i][j],其中i表示当前考虑到第i个物品,j表示当前背包容量为j,dp[i][j]表示在前i个物品中,背包容量为j时的最大价值。 2. 状态转移方程 对于每个物品,我们可以选择将其放入背包,也可以选择不放入背包,因此状态转移方程如下: 如果不将第i个物品放入背包,则 dp[i][j] = dp[i - 1][j] 即前i-1个物品已经在容量为j的背包中的最大价值就是dp[i - 1][j]。 如果将第i个物品放入背包,则 dp[i][j] = dp[i-1][j-w[i]] + v[i] 即前i-1个物品在容量为j-w[i]的背包中的最大价值加上第i个物品的价值v[i]。 最终的状态转移方程为: dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) 3. 边界条件 当物品数量为0时,dp[0][j]都等于0;当背包容量为0时,dp[i][0]都等于0。 4. 求解最优解 最终的最大价值为dp[n][W],其中n表示物品数量,W表示背包容量。 5. 代码实现 以下是01背包问题动态规划代码实现,其中w和v分别表示物品的重量和价值,n和W表示物品数量和背包容量: ```python def knapsack(w, v, n, W): dp = [[0] * (W+1) for _ in range(n+1)] for i in range(1, n+1): for j in range(1, W+1): if j < w[i-1]: dp[i][j] = dp[i-1][j] else: dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1]) return dp[n][W] ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值