0-1背包问题

有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。

定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
  • 第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:

空间优化

在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,

因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。

public int package(int W, int N, int[] weights, int[] values) {
    int[] dp = new int[W + 1];
    for (int i = 1; i <= N; i++) {
        int w = weights[i - 1], v = values[i - 1];
        for (int j = W; j >= 1; j--) {
            if (j >= w) {
                dp[j] = Math.max(dp[j], dp[j - w] + v);
            }
        }
    }
    return dp[W];
}

1、416. Partition Equal Subset Sum

Description:

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:

  1. Each of the array element will not exceed 100.
  2. The array size will not exceed 200.

Example 1:

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

Example 2:

Input: [1, 2, 3, 5]

Output: false

Explanation: The array cannot be partitioned into equal sum subsets.

分析:

看成一个背包大小为 sum/2 的 0-1 背包问题。

代码如下:

public class Solution {

    public boolean canPartition(int[] nums) {
        int sum = numSum(nums);
        if (0 != sum%2){
            return false;
        }
        int W = sum >> 1; //容量
        boolean[] dp = new boolean[W+1];
        dp[0] = true;
        //一个物品只能用一次
        for (int num : nums){
            //从后往前,先计算 dp[i] 再计算 dp[i-num]
            for (int i = W; i >= num; i--){
                dp[i] = dp[i] || dp[i-num];
            }
        }
        return dp[W];
    }

    private int numSum(int[] arr){
        int sum = 0;
        for (int a : arr){
            sum += a;
        }
        return sum;
    }
}

2、LeetCode322. Coin Change

Description:

You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

Example 1:

Input: coins = [1, 2, 5], amount = 11

Output: 3
 
Explanation: 11 = 5 + 5 + 1

Example 2:

Input: coins = [2], amount = 3

Output: -1

Note:

You may assume that you have an infinite number of each kind of coin.

分析:

硬币可以重复使用,所以这是一个完全背包问题。完全背包只需要将 0-1 背包的逆序遍历 dp 数组改为正序遍历即可。

代码如下:

class Solution {
    public int coinChange(int[] coins, int amount) {
        if (coins.length < 1){
            return -1;
        }
        int[] dp = new int[amount + 1];
        dp[0] = 0;
        for (int i = 1; i <= amount; i++){
            dp[i] = Integer.MAX_VALUE  - 1;
            for (int coin : coins){
                if (i - coin >= 0){
                    dp[i] = Math.min(dp[i-coin] + 1, dp[i]);
                }
            }
        }
        return dp[amount] == Integer.MAX_VALUE  - 1 ? -1 : dp[amount];
    }
}

3、LeetCode518. Coin Change 2

Description:

You are given coins of different denominations and a total amount of money. Write a function to compute the number of combinations that make up that amount. You may assume that you have infinite number of each kind of coin.

Example 1:

Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

Example 2:

Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

Example 3:

Input: amount = 10, coins = [10] 
Output: 1

分析:

完全背包问题,使用 dp 记录可达成目标的组合数目。

代码如下:

class Solution {
    public int change(int amount, int[] coins) {
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for (int coin : coins){
            for (int i = 1; i <= amount; i++){
                if (i >= coin){
                    dp[i] += dp[i-coin];
                }
            }
        }  
        return dp[amount];
    }
}

4、LeetCode494. Target Sum

Description:

You are given a list of non-negative integers, a1, a2, ..., an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.

Find out how many ways to assign symbols to make sum of integers equal to target S.

Example 1:

Input: nums is [1, 1, 1, 1, 1], S is 3. 
Output: 5
Explanation: 

-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

There are 5 ways to assign symbols to make the sum of nums be target 3.

Note:

  1. The length of the given array is positive and will not exceed 20.
  2. The sum of elements in the given array will not exceed 1000.
  3. Your output answer is guaranteed to be fitted in a 32-bit integer.

分析:

该问题转换为子集求和的问题,从而使用 0-1 背包的方法来求解。

先将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。

代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = getSum(nums);
        if (sum < S || (S + sum)%2 == 1){
            return 0;
        }
        int W = (S+sum) >> 1;
        int[] dp = new int[W+1];
        dp[0] = 1;
        for (int num : nums){
            for (int i = W; i >= num; i--){
                dp[i] = dp[i] + dp[i-num];
            }
        }
        return dp[W];

    }

    private int getSum(int[] nums){
        int sum = 0;
        for (int num : nums){
            sum += num;
        }
        return sum;
    }
}

5、LeetCode474. Ones and Zeroes

Description:

In the computer world, use restricted resource you have to generate maximum benefit is what we always want to pursue.

For now, suppose you are a dominator of m 0s and n 1s respectively. On the other hand, there is an array with strings consisting of only 0s and 1s.

Now your task is to find the maximum number of strings that you can form with given m 0s and n 1s. Each 0 and 1 can be used at most once.

Note:

  1. The given numbers of 0s and 1s will both not exceed 100
  2. The size of given string array won't exceed 600.

Example 1:

Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4

Explanation: This are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are “10,”0001”,”1”,”0”

Example 2:

Input: Array = {"10", "0", "1"}, m = 1, n = 1
Output: 2

Explanation: You could form "10", but then you'd have nothing left. Better form "0" and "1".

分析:

多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。

代码如下:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        if (null == strs || strs.length < 1){
            return 0;
        }
        int[][] dp = new int[m+1][n+1];
        for (String str : strs){
            int oneNum = 0;
            int zeroNum = 0;
            for (char c : str.toCharArray()){
                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], dp[i-zeroNum][j-oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}

6、LeetCode139. Word Break

Description:

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

Note:

  • The same word in the dictionary may be reused multiple times in the segmentation.
  • You may assume the dictionary does not contain duplicate words.

Example 1:

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode"can be segmented as "leet code".

Example 2:

Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
             Note that you are allowed to reuse a dictionary word.

Example 3:

Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false

分析:

       wordDict 中的单词没有使用次数的限制,这也是一个完全背包问题。该问题涉及到字典中单词的使用顺序,也就是说物品必须按一定顺序放入背包中。

        在求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中。

代码如下:

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        if (null == wordDict || wordDict.size() < 1){
            return false;
        }
        int length = s.length();
        boolean[] dp = new boolean[length+1];
        dp[0] = true;
        for (int i = 1; i <= length; i++){
            //对物品的迭代放在内层
            for (String str : wordDict){
                int len =  str.length();
                if (len <= i && str.equals(s.substring(i-len, i))){
                    dp[i] = dp[i] || dp[i-len];
                }
            }
        }
        return dp[length];
    }
}

7、LeetCode377. Combination Sum IV

Description:

Given an integer array with all positive numbers and no duplicates, find the number of possible combinations that add up to a positive integer target.

Example:

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

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.

Follow up:

What if negative numbers are allowed in the given array?
How does it change the problem?
What limitation we need to add to the question to allow negative numbers?

Credits:

Special thanks to @pbrother for adding this problem and creating all test cases.

分析:

顺序的完全背包。

代码如下:

class Solution {
    public int combinationSum4(int[] nums, int target) {
        if (null == nums || nums.length < 1){
            return 0;
        }
        int[] dp = new int[target + 1];
        dp[0] = 1;
        Arrays.sort(nums);
        for (int i = 1; i <= target; i++){
            for (int j = 0; j < nums.length && nums[j] <= i; j++){
                dp[i] += dp[i-nums[j]];
            }
        }
        return dp[target];
    }
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值