动态规划-背包问题

01背包

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

2.每个物品只有一个,只有选与不选两个选择,如下,取最大值

  • 不选物品i:dp[i][j] = dp[i-1][j];
  • 选物品i:dp[i][j] = dp[i-1][j-wight[i]]+val[j];

3.状态转移方程 : dp[i][j] = Math.max(dp[i][j], dp[i-1][j-wight[i]]+val[j] );

4.注意dp[i][j]的初始化

5.遍历顺序:推荐先遍历物品,再遍历背包

优化:
每个取值都依赖于正上方和左上角元素,可以进行空间压缩,从上到下,从右到左

将其转换成一维dp数组:

dp[j] = Math.max(dp[j], dp[j-weight[i]] + val[i]);

dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。

注意:一维dp的遍历顺序与二维dp遍历背包的顺序不一样。

  • 二维dp从小到大遍历背包容量
  • 一维dp倒序遍历,只为了保证物品i只被放入一次!!!

注意:一维dp 只能先遍历物品,再遍历背包容量!!!

416.分割等和子集 

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

本题中每一个元素的数值既是重量,也是价值。

1.dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]

2.递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

3.初始化:首先dp[0]一定是0。如果题目给的价值有负数,那么非0下标就要初始化为负无穷。本题 只包含正整数,所以非0下标的元素初始化为0就可以了。

4.先遍历物品;再遍历背包容量,且倒序。

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num : nums){
            sum += num;
        }
        if(sum % 2 ==1) return false;
        int target = sum /2;
        int[] dp = new int[target+1];
        for(int i = 0; i<nums.length; i++){
            for(int j=target; j>=nums[i]; j--){
                dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
            }
            if(dp[target]==target) return true;
        }
        
        return dp[target]==target;
    }
}

 1049.最后一块石头的重量II- 力扣(LeetCode)

本题与上一题是一个意思。只是对dp[target]的处理方式不同。

只要尽量让石头分成重量相同的两堆,相撞之后剩下的石头就会是最小的。

在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int i : stones) {
            sum += i;
        }
        int target = sum >> 1;
        //初始化dp数组
        int[] dp = new int[target + 1];
        for (int i = 0; i < stones.length; i++) {
            //采用倒序
            for (int j = target; j >= stones[i]; j--) {
                //两种情况,要么放,要么不放
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        return sum - 2 * dp[target];
    }
}

 494.目标和. - 力扣(LeetCode)

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为x的背包,有几种方法

考虑边界:

if ((target + sum) % 2 == 1) return 0; // 此时没有方案
if (abs(target) > sum) return 0; // 此时没有方案
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];

        //如果target的绝对值大于sum,那么是没有方案的
        if (Math.abs(target) > sum) return 0;
        //如果(target+sum)除以2的余数不为0,也是没有方案的
        if ((target + sum) % 2 == 1) return 0;

        int bagSize = (target + sum) / 2;
        int[] dp = new int[bagSize + 1];
        dp[0] = 1;

        for (int i = 0; i < nums.length; i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[bagSize];
    }
}

完全背包

与01背包不同的点在于每种物品有无限件。因此,要从小到大、去正序遍历。

322. 零钱兑换. - 力扣(LeetCode)

1.dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

2.dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

3.首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;

下标非0的元素都应该初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount+1];
        dp[0]=0;
        for(int i=1; i<=amount; i++){
            dp[i] = max;
        }
        for(int i=0; i<coins.length; i++){
            for(int j=coins[i]; j<=amount; j++){
                // 只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                // 如果缺少这个if语句会报错,因为max+1=-max
                if(dp[j-coins[i]] != max) dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

139.单词拆分. - 力扣(LeetCode) 

单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。

拆分时可以重复使用字典中的单词,说明就是一个完全背包!

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

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

3.dp[0]一定要为true,否则递推下去后面都都是false了.

4.本题一定是 先遍历 背包,再遍历物品。因为强调物品之间顺序,即排列问题。

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> word = 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++){
                if(dp[j] && word.contains(s.substring(j,i))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

多重背包

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

先遍历物品再遍历背包,作为01背包处理

是01背包里面在加一个for循环遍历一个每种商品的数量。 

总结:

01背包:

二维dp时,两个for循环的先后顺序可以颠倒。但一维dp只能先遍历物品,再遍历背包容量,否则每个dp[j]就只会放入一个物品。并且一维dp的背包容量只能倒序遍历,从而保证每个物品最多放入一次。

纯完全背包的两个for循环的先后顺序可以颠倒,因为不会影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值