背包问题算法


1 01背包

🚀 题目链接https://www.acwing.com/problem/content/2/
🚀 代码

  • 二维dp
    // n为物品数量,maxV为背包容量,v[i]、w[i]分别为第i件物品的容量和价值
    public int zeroOneKnapsack(int n, int maxV, int[] v, int[] w) {
        // 动态函数,表示将第i个物品放入容量为j的背包中的最大价值
        int[][] dp = new int[n + 1][maxV + 1];
    
        for (int i = 1;i <= n;i++) {
            for (int j = 1;j <= maxV;j++) {
                if (v[i - 1] <= j) {
                    // 能放入,则获取放入和不放入的较大价值
                    dp[i][j] = Math.max(
                            // 放入,这里是dp[i-1]
                        	// dp[i-1]表示在上一件物品放或没放的基础上放入当前物品,因为一件物品只能放一次
                            dp[i - 1][j - v[i - 1]] + w[i - 1],
                            // 不放
                            dp[i - 1][j]);
                } else {
                    // 放不下,最大价值就是放前一个物品时的价值
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n][maxV];
    }
    
  • 一维dp
    public int zeroOneKnapsack(int n, int maxV, int[] v, int[] w) {
        // 一维动态函数,表示将容量为j的背包中的最大价值
        int[] dp = new int[maxV + 1];
    
        for (int i = 1;i <= n;i++) {
            // 从后往前遍历,如果从前往后遍历会取到重复的物件
            for (int j = maxV;j <= maxV && j >= v[i - 1];j--) {
                dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]);
            }
        }
        return dp[maxV];
    }
    

1.1 分割等和子集

🚀 题目链接https://leetcode.cn/problems/partition-equal-subset-sum/
🚀 代码

class Solution {
    public boolean canPartition(int[] nums) {
        // 统计数组和和,并求出最大值
        int sum = 0, maxVal = 0;
        for (int num : nums) {
            sum += num;
            maxVal = Math.max(maxVal, num);
        }

        // 和是奇数,肯定不能分割
        if (sum % 2 != 0) {
            return false;
        }

        int target = sum >> 1, n = nums.length;
        // 最大值比和的一半还大,肯定不能分割
        if (maxVal > target) {
            return false;
        }

        // 01背包的做法
        // dp(x,y),x表示数组元素的索引,y表示目标和
        // dp(x,y)表示在[0,x]之间是否能选择若干个数组元素的和为y
        boolean[][] dp = new boolean[nums.length][target + 1];

        // 初始化dp
        for (int i = 0;i < n;i++) {
            // 目标和为0时(不可能达到0)
            // 设置为true是要保证j刚好等于num时为true
            dp[i][0] = true;
        }

        // 只有一个元素(第一个元素)时,如果目标和等于刚好等于它,肯定能分割
        dp[0][nums[0]] = true;
        for (int i = 1;i < n;i++) {
            int num = nums[i];
            for (int j = 1;j <= target;j++) {
                if (j >= num) {
                    // 可以选择当前num,则不选与选的情况进行与运算,有一个true即可
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
                } else {
                    // 不能选入当前num,则直接等于i-1时对应的dp
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[n - 1][target];
    }
}

1.2 目标和

🚀 题目链接https://leetcode.cn/problems/target-sum
🚀 代码

  • 二维dp
    class Solution {
        public int findTargetSumWays(int[] nums, int target) {
            // 统计数组全部元素和(元素都是非负数)
            int sum = 0;
            for (int num : nums) {
                sum += num;
            }
    
            int diff = sum - target;
            if (diff < 0 || diff % 2 != 0) {
                // sum和目标值的差为负数或奇数,则肯定不能算出target
                return 0;
            }
    
            // neg为添加符号的元素值和
            int n = nums.length, neg = diff >> 1;
            // 01背包选方案做法,dp(x,y)表示在[0,x]范围内和为neg的元素方案数
            int[][] dp = new int[n + 1][neg + 1];
            // 初始化dp,默认一种方案
            dp[0][0] = 1;
    
            for (int i = 1;i <= n;i++) {
                int num = nums[i - 1];
                for (int j = 0;j <= neg;j++) {
                    // 默认赋值为不选入num的方案数
                    dp[i][j] = dp[i - 1][j];
                    if (j >= num) {
                        // 能选择当前num,则再加上选入num的方案数
                        dp[i][j] += dp[i - 1][j - num];
                    } 
                }
            }
    
            return dp[n][neg];
        }
    }
    
  • 一维dp
    class Solution {
        public int findTargetSumWays(int[] nums, int target) {
            // 统计数组全部元素和(元素都是非负数)
            int sum = 0;
            for (int num : nums) {
                sum += num;
            }
    
            int diff = sum - target;
            if (diff < 0 || diff % 2 != 0) {
                // sum和目标值的差为负数或奇数,则肯定不能算出target
                return 0;
            }
    
            // neg为添加符号的元素值和
            int n = nums.length, neg = diff >> 1;
            // 01背包选方案做法,一维dp
            int[] dp = new int[neg + 1];
            // 初始化dp,默认有一种方案
            dp[0] = 1;
    
            for (int num : nums) {
                // 一维dp要反过来遍历
                for (int j = neg;j >= 0;j--) {
                    if (j >= num) {
                        // 能选择当前num,则再加上选入num的方案数
                        dp[j] += dp[j - num];
                    } 
                }
            }
    
            return dp[neg];
        }
    }
    

2 完全背包

🚀 题目链接https://www.acwing.com/problem/content/3/
🚀 代码

  • 二维dp
    public int completeKnapsack(int n, int maxV, int[] v, int[] w) {
        // 动态函数,表示将第i个物品放入容量为j的背包中的最大价值
        int[][] dp = new int[n + 1][maxV + 1];
    
        for (int i = 1;i <= n;i++) {
            for (int j = 1;j <= maxV;j++) {
            	// 这里物品是否能放的条件不能放到for循环中
                if (v[i - 1] <= j) {
                    // 能放入,获取放入和不放入的较大价值
                    dp[i][j] = Math.max(
                        // 放入,这里是dp[i],不是dp[i-1](01背包的话是dp[i-1])
                        // dp[i]表示在当前物品放或没放的基础上再放入当前物品,因为一件物品可以放多次
                        dp[i][j - v[i - 1]] + w[i - 1],
                        // 不放
                        dp[i - 1][j]);
                } else {
                    // 放不下,最大价值就是放前一个物品时的价值
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n][maxV];
    }
    
  • 一维dp
    public int completeKnapsack(int n, int maxV, int[] v, int[] w) {
        // 一维动态函数,表示将第i种物品放入容量为j的背包中的最大价值
        int[] dp = new int[maxV + 1];
    
        for (int i = 1;i <= n;i++) {
            // 01背包中内循环是倒序,这里是升序,因为可以重复放入物品
            for (int j = 1;j <= maxV;j++) {
                if (j >= v[i - 1]) {
                    dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]);
                }
            }
        }
        return dp[maxV];
    }
    

3 多重背包

🚀 题目链接https://www.acwing.com/problem/content/4/
🚀 代码

  • 朴素解法①:在01背包中嵌套一层循环(一个种类中每一件物品逐渐合并的过程)
    // c[i]表示第i件物品的数量
    public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) {
        // 动态函数
        int[] dp = new int[maxV + 1];
    
        // 将每一种物品根据数量合并,转化成01背包
        for (int i = 1;i <= n;i++) {
            for (int j = maxV;j >= v[i - 1];j--) {
                // k表示物品数量,注意k*v不能大于背包当前的容量
                for (int k = 1;k <= c[i - 1] && k * v[i - 1] <= j;k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * v[i - 1]] + k * w[i - 1]);
                }
            }
        }
        return dp[maxV];
    }
    
  • 朴素解法②:将每一种物品一件一件地进行拆分,再直接01背包
    public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) {
        // 动态函数
        int[] dp = new int[maxV + 1];
        List<Integer> mergeV = new ArrayList<>();
        List<Integer> mergeW = new ArrayList<>();
        // 拆分后的物品件数
        int newN = 0;
    
        // 将第i种物品拆成c[i]件
        for (int i = 0;i < n;i++) {
            for (int j = 0;j < c[i];j++) {
                mergeV.add(v[i]);
                mergeW.add(w[i]);
                newN++;
            }
        }
        // 直接01背包
        for (int i = 1;i <= newN;i++) {
            for (int j = maxV;j >= mergeV.get(i - 1);j--) {
                dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1));
            }
        }
        return dp[maxV];
    }
    
  • 二进制优化:在朴素解法②的基础上优化拆分的算法(不是一件一件地拆分)
    public int multipleKnapsack(int n, int maxV, int[] v, int[] w, int[] c) {
        // 动态函数
        int[] dp = new int[maxV + 1];
        List<Integer> mergeV = new ArrayList<>();
        List<Integer> mergeW = new ArrayList<>();
        // 拆分后的物品件数
        int newN = 0;
    
        // 根据二进制分解拆分
        for (int i = 0;i < n;i++) {
            for (int j = 1;j < c[i];j <<= 1) {
                mergeV.add(j * v[i]);
                mergeW.add(j * w[i]);
                newN++;
                // 拆分后数量减少
                c[i] -= j;
            }
    
            if (c[i] != 0) {
                // 还有剩余数量/当前物品数量为1,直接合并
                mergeV.add(c[i] * v[i]);
                mergeW.add(c[i] * w[i]);
                newN++;
            }
        }
    
        // 直接01背包
        for (int i = 1;i <= newN;i++) {
            for (int j = maxV;j >= mergeV.get(i - 1);j--) {
                dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1));
            }
        }
        return dp[maxV];
    }
    

4 混合背包

🚀 题目链接https://www.acwing.com/problem/content/7/
🚀 代码

public int mixKnapsack(int n, int maxV, int[] v, int[] w, int[] c) {
    // 动态函数
    int[] dp = new int[maxV + 1];
    List<Integer> mergeV = new ArrayList<>();
    List<Integer> mergeW = new ArrayList<>();
    int newN = 0;

    // 修改物品的数量,-1改成1,0(无限)改成maxV/v(最多能放的物品件数)
    for (int i = 0;i < n;i++) {
        if (c[i] == -1) {
            c[i] = 1;
        } else if (c[i] == 0) {
            c[i] = maxV / v[i];
        }
    }

    // 后面直接套多重背包的二进制优化解法即可
    for (int i = 0;i < n;i++) {
        for (int j = 1;j < c[i];j <<= 1) {
            mergeV.add(j * v[i]);
            mergeW.add(j * w[i]);
            newN++;
            c[i] -= j;
        }

        if (c[i] != 0) {
            mergeV.add(c[i] * v[i]);
            mergeW.add(c[i] * w[i]);
            newN++;
        }
    }

    // 01背包
    for (int i = 1;i <= newN;i++) {
        for (int j = maxV;j >= mergeV.get(i - 1);j--) {
            dp[j] = Math.max(dp[j], dp[j - mergeV.get(i - 1)] + mergeW.get(i - 1));
        }
    }
    return dp[maxV];
}

5 二维费用背包

🚀 题目链接https://www.acwing.com/problem/content/8/
🚀 代码

// m[i]表示第i件物品的重量
public int twoDimensionCostKnapsack(int n, int maxV, int maxM, int[] v, int[] m, int[] w) {
    // 二维动态数组,记住容量为j,重量为k的背包能承受的最大价值
    int[][] dp = new int[maxV + 1][maxM + 1];

    // 三重循环
    for (int i = 1;i <= n;i++) {
        // 遍历容量
        for (int j = maxV;j <= maxV && j >= v[i - 1];j--) {
            // 遍历重量
            for (int k = maxM;k <= maxM && k >= m[i - 1];k--) {
                // 套01背包的模板
                dp[j][k] = Math.max(dp[j][k], dp[j - v[i - 1]][k - m[i - 1]] + w[i - 1]);
            }
        }
    }
    return dp[maxV][maxM];
}

6 分组背包

🚀 题目链接https://www.acwing.com/problem/content/9/
🚀 代码

// s[i]表示第i组中物品件数,v[i][j]、w[i][j]分别表示第i组中第j件物品的容量和价值
public int groupKnapsack(int n, int maxV, int[] s, int[][] v, int[][] w) {
    // 一维dp,类似01背包
    int[] dp = new int[maxV + 1];

    // 三重循环,外循环遍历每一组
    for (int i = 1;i <= n;i++) {
        // 遍历背包容量,一维dp,需要倒序遍历
        for (int j = maxV;j >= 1;j--) {
            // 遍历每一组中每一件物品,在上一组的物品基础上选择
            // 类似于01背包,一件物品只能选一次,但一组内最多选一件物品,所以需要再套一重循环
            for (int k = 0;k < s[i - 1];k++) {
                // 这里是否能放入的条件不能放到for循环中,可能该组的第一件物品就放不下,循环就直接结束
                if (j >= v[i - 1][k]) {
                    dp[j] = Math.max(dp[j], dp[j - v[i - 1][k]] + w[i - 1][k]);
                }
            }
        }
    }
    return dp[maxV];
}

7 求最优方案数

🚀 题目链接https://www.acwing.com/problem/content/11/
🚀 代码

// 01背包基础上求最优方案数
public int getBestCnt(int n, int maxV, int[] v, int[] w) {
    // 一维dp
    int[] dp = new int[maxV + 1];
    // 记住背包为i时的最优方案数
    int[] bestCnt = new int[maxV + 1];
    // 初始化方案数全为1:不放当前物品,直接继承上一件物品(这种方案肯定有,如果放了价值更大再修改)
    Arrays.fill(bestCnt, 1);

    // 01背包
    for (int i = 1;i <= n;i++) {
        for (int j = maxV;j <= maxV && j >= v[i - 1];j--) {
            // 记住放入当前物品后的价值
            int putVal = dp[j - v[i - 1]] + w[i - 1];
            if (putVal > dp[j]) {
                // 如果放入后价值比不放要大,那肯定放
                dp[j] = putVal;
                // 方案数更新为放入当前物品后剩余容量对应的方案数
                bestCnt[j] = bestCnt[j - v[i - 1]];
            } else if (putVal == dp[j]) {
                // 如果放和不放的价值一样
                // 则需要在原来(不放)的基础上加上放入当前物品后剩余容量对应的方案数
                bestCnt[j] = (bestCnt[j] + bestCnt[j - v[i - 1]]) % (int) (1e9 + 7) ;
            }
        }
    }
    return bestCnt[maxV];
}

continue…

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值