当前位置: 首页 > article >正文

背包问题的详细理解(无基础->详细掌握)

一、0/1 背包问题

在0/1背包问题中,每个物品只能选一次,要么选择放入背包,要么不选。我们需要找到一种选择,使得背包的总价值最大且不超过背包的容量。

问题描述
  • n 个物品,每个物品有重量 w[i] 和价值 v[i]
  • 背包容量为 W,每个物品只能选择一次。
状态转移方程解释
  • 状态定义dp[i][j] 表示前 i 个物品中,容量为 j 的背包可以达到的最大价值。
  • 状态转移方程
    • 不选第 i 个物品dp[i][j] = dp[i-1][j]
      • 在这种情况下,我们选择不包含第 i 个物品。因此,容量为 j 的背包在前 i 个物品中可以达到的最大价值就是在前 i-1 个物品中最大价值。也就是说,我们直接沿用之前的值,不做任何更改。
    • 选择第 i 个物品dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])
      • 在这种情况下,我们需要考虑是否加入第 i 个物品。如果我们决定加入第 i 个物品,那么剩余的容量将减少为 j - w[i]。在剩余容量下,我们需要找到前 i-1 个物品中可以取得的最大价值,并加上当前物品的价值 v[i]。最终,我们通过比较选择和不选择第 i 个物品时的最大价值,取两者中的最大值来更新状态。
      • 这里的核心思想在于利用已知的最优子问题解来推导当前问题的最优解,这也是动态规划的主要特征之一。在解决背包问题时,这种逐步增加物品并比较选择与不选择的过程,帮助我们有效地找到每种容量下的最大价值。
public class Knapsack01 {
    public static void main(String[] args) {
        int[] weights = {1, 3, 4, 5};
        int[] values = {15, 50, 60, 90};
        int capacity = 8;
        
        int maxValue = knapsack01(weights, values, capacity);
        System.out.println("\u6700\u5927\u4ef7\u503c\u4e3a:" + maxValue);
    }

    public static int knapsack01(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        int[][] dp = new int[n + 1][capacity + 1];

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= capacity; j++) {
                if (weights[i - 1] <= j) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        // \u53ef\u89c6\u5316 dp \u8868\u683c
        System.out.println("\u52a8\u6001\u89c4\u5212\u8868\u683c\uff1a");
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= capacity; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println();
        }

        return dp[n][capacity];
    }
}
表格记录数据变化
物品数/背包容量012345678
0000000000
101515151515151515
201515506565656565
30151550657575110125
401515506590105110140

二、完全背包问题

在完全背包问题中,每个物品可以选取多次。问题的思路与0/1背包类似,状态定义相同,但状态转移有所不同。

状态转移方程
  • 状态定义:与 0/1 背包类似,dp[i][j] 表示前 i 个物品中容量为 j 的背包的最大价值。
  • 状态转移方程dp[i][j] = max(dp[i-1][j], dp[i][j - w[i]] + v[i])
    • 不选择第 i 个物品dp[i][j] = dp[i-1][j],此时的解释与 0/1 背包一致,表示直接沿用前 i-1 个物品的最佳解,意味着我们并不将当前物品纳入背包的选择中。
    • 选择第 i 个物品:与 0/1 背包不同,这里可以多次选择相同的物品。因此,我们在选择第 i 个物品后,还可以继续选择这个物品。在减少容量的基础上,我们加上当前物品的价值 v[i],并查看之前相应容量下的最佳状态 dp[i][j - w[i]]。这意味着在完全背包问题中,我们不仅可以使用每个物品一次,还可以无限制地重复使用,这样可以更有效地利用背包的剩余容量。
    • 完全背包问题中的动态规划求解方法与 0/1 背包的主要区别在于对物品的重复使用。我们通过更新状态,使得每个物品都可以被多次加入,只要当前容量足够容纳它。这使得求解过程更为灵活,也需要更为复杂的考虑,以确保所有可能的情况都得到评估。
public class CompleteKnapsack {
    public static void main(String[] args) {
        int[] weights = {1, 3, 4, 5};
        int[] values = {15, 50, 60, 90};
        int capacity = 8;

        int maxValue = completeKnapsack(weights, values, capacity);
        System.out.println("最大价值为:" + maxValue);
    }

    public static int completeKnapsack(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < n; i++) {
            for (int j = weights[i]; j <= capacity; j++) {
                dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
            }
        }

        /// 可视化 dp 表格        System.out.println("动态规划表格:");
        for (int j = 0; j <= capacity; j++) {
            System.out.print(dp[j] + "\t");
        }
        System.out.println();

        return dp[capacity];
    }
}
表格记录数据变化
背包容量012345678
最大价值01530506590105120140

三、多重背包问题

在多重背包问题中,每个物品的数量是有限的,不同于0/1背包和完全背包。

状态转移方程
  • 状态定义dp[j] 表示容量为 j 的背包可达到的最大价值。
  • 状态转移方程
    • 对于每个物品 i,我们可以选择 0~k 个,其中 1 <= k <= quantities[i],并且需要满足 k * weights[i] <= j
    • 我们通过一个嵌套循环来模拟不同数量的物品加入背包的情况,更新背包容量为 j 时的最大价值:dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i])
    • 在多重背包问题中,每个物品的数量是有限的,因此我们需要用双重循环去遍历每个物品的可能数量,并求得最大价值。这样,我们既能保证所有物品的数量限制得到满足,同时也能确保每种选择下的最大价值被计算出来。
    • 这种方式通过逐层叠加每种物品的不同数量,从而在有限的物品中求得最大价值。这种方法的挑战在于要平衡物品数量的限制和容量的限制,以找到每种情况下的最优解。相比于 0/1 背包和完全背包,多重背包需要对每个物品的使用次数加以控制,因此状态的更新逻辑更为复杂,需要嵌套循环去计算每个可能的状态。
public class MultipleKnapsack {
    public static void main(String[] args) {
        int[] weights = {1, 3, 4};
        int[] values = {15, 50, 60};
        int[] quantities = {2, 3, 1};
        int capacity = 8;

        int maxValue = multipleKnapsack(weights, values, quantities, capacity);
        System.out.println("最大价值为:" + maxValue);
    }

    public static int multipleKnapsack(int[] weights, int[] values, int[] quantities, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];

        for (int i = 0; i < n; i++) {
            for (int j = capacity; j >= weights[i]; j--) {
                for (int k = 1; k <= quantities[i] && k * weights[i] <= j; k++) {
                    dp[j] = Math.max(dp[j], dp[j - k * weights[i]] + k * values[i]);
                }
            }
        }

        // 可视化 dp 表格
        System.out.println("动态规划表格:");
        for (int j = 0; j <= capacity; j++) {
            System.out.print(dp[j] + "\t");
        }
        System.out.println();

        return dp[capacity];
    }
}
表格记录数据变化
背包容量012345678
最大价值01530506590105115125

四、分数背包问题(贪心算法)

分数背包问题不同于其他几种背包问题,在这种情况下,物品可以被部分选择。因此,使用贪心算法来实现。

问题描述
  • 按照单位重量的价值来对物品进行排序,然后依次将物品加入背包。
状态转移方程解释
  • 贪心策略:由于分数背包可以部分选择物品,动态规划并不是最优解法,而是使用贪心算法。
  • 策略:按照单位重量的价值对物品进行排序,优先选择单位价值最高的物品。这样可以保证每次选择对背包总价值的增量最大。
  • 在贪心策略中,我们从单位重量价值最高的物品开始选择,将其尽可能地加入到背包中。如果背包容量不足以容纳整个物品,则选择部分物品来填充剩余空间。
  • 每次选择物品时,我们减小背包的容量,可以部分选择该物品以使背包的总价值最大化。这种方法的核心思想是尽量使单位价值最高的物品占据更多的容量,这样可以在有限容量内获得最大的收益。这也是贪心算法在分数背包问题中的最优解策略。
  • 分数背包与其他背包问题不同的地方在于它允许部分选择,因此不再需要使用动态规划,而是通过排序和逐步贪心地选择来实现最优解。这使得问题求解的时间复杂度得到了有效降低。
import java.util.Arrays;
import java.util.Comparator;

public class FractionalKnapsack {
    static class Item {
        int weight, value;
        Item(int weight, int value) {
            this.weight = weight;
            this.value = value;
        }
    }

    public static void main(String[] args) {
        Item[] items = {
            new Item(1, 15),
            new Item(3, 50),
            new Item(4, 60),
            new Item(5, 90)
        };
        int capacity = 8;

        double maxValue = fractionalKnapsack(items, capacity);
        System.out.println("最大价值为:" + maxValue);
    }

    public static double fractionalKnapsack(Item[] items, int capacity) {
        Arrays.sort(items, Comparator.comparingDouble(i -> -1.0 * i.value / i.weight));

        double maxValue = 0.0;
        int remainingCapacity = capacity;

        for (Item item : items) {
            if (remainingCapacity >= item.weight) {
                maxValue += item.value;
                remainingCapacity -= item.weight;
            } else {
                maxValue += (double) item.value * remainingCapacity / item.weight;
                break;
            }
        }

        return maxValue;
    }
}
表格记录数据变化
物品序号重量价值单位重量价值已选重量剩余容量当前总价值
111515.01715.0
235016.673465.0
346015.040125.0

五、使用背包问题解法解决的其他算法问题

  1. 子集和问题

    • 问题描述:给定一个整数集合,判断是否存在一个子集,使得其和等于给定的目标值。这个问题可以看作0/1背包问题的变形,每个物品的重量等于它的值,目标是找到和等于目标值的组合。
状态转移方程解释
  • 状态定义dp[i][j] 表示前 i 个数中是否可以找到一个子集,使得其和为 j

  • 状态转移方程dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]]

    • 不选择当前元素:我们直接延续前 i-1 个元素是否可以达到和为 j 的状态。
    • 选择当前元素:如果选择第 i 个元素,那么需要看 j - nums[i - 1] 是否能由前 i-1 个元素构成。如果可以构成,则当前状态为 true,表示可以找到满足条件的子集。
    • 这个问题的核心思想是将问题转化为一个决策问题,通过选择或不选择每个元素来判断是否可以达到目标和。动态规划表格的每个状态都基于之前的状态更新,从而逐步解决整个问题。
    public class SubsetSum {
        public static void main(String[] args) {
            int[] nums = {3, 34, 4, 12, 5, 2};
            int target = 9;
            boolean result = subsetSum(nums, target);
            System.out.println("是否存在和为目标值的子集:" + result);
        }
    
        public static boolean subsetSum(int[] nums, int target) {
            int n = nums.length;
            boolean[][] dp = new boolean[n + 1][target + 1];
    
            for (int i = 0; i <= n; i++) {
                dp[i][0] = true;
            }
    
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= target; j++) {
                    if (nums[i - 1] <= j) {
                        dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
                    } else {
                        dp[i][j] = dp[i - 1][j];
                    }
                }
            }
    
            // 可视化 dp 表格
            System.out.println("动态规划表格:");
            for (int i = 0; i <= n; i++) {
                for (int j = 0; j <= target; j++) {
                    System.out.print((dp[i][j] ? 1 : 0) + "\t");
                }
                System.out.println();
            }
    
            return dp[n][target];
        }
    }
    
    表格记录数据变化
    数组长度/目标值0123456789
    01000000000
    11001000000
    21001000000
    31001100100
    41001100110
    51011110111
    61111111111
  1. 找零问题

    • 问题描述:给定不同面值的硬币和一个总金额,求最少需要多少枚硬币可以凑成这个金额。如果没有硬币组合能组成总金额,则返回 -1。这个问题可以看作完全背包问题,每种面值的硬币可以选择无限次。
状态转移方程解释
  • 状态定义dp[j] 表示凑成金额 j 所需的最少硬币数。

  • 状态转移方程dp[j] = min(dp[j], dp[j - coin] + 1)

    • 对于每种硬币面值,计算在达到金额 j 时选择当前硬币是否可以减少硬币数量。dp[j - coin] 表示不选择当前硬币的最小数量,而 +1 表示我们加入了当前硬币。
    • 通过这种方式,我们能够找到在给定的硬币面值下,凑成目标金额所需的最少硬币数。这个问题的难点在于要同时考虑多个面值的硬币,找到使得数量最小的组合。
    • 这种动态规划的解法通过从金额为 0 开始逐步向上递增,确保在每个阶段都找到最优解。这种方法也具有较高的时间效率,能够在有限的硬币面值下快速找到最优方案。
    public class CoinChange {
        public static void main(String[] args) {
            int[] coins = {1, 2, 5};
            int amount = 11;
            int result = coinChange(coins, amount);
            System.out.println("最少硬币数:" + result);
        }
    
        public static int coinChange(int[] coins, int amount) {
            int max = amount + 1;
            int[] dp = new int[amount + 1];
            Arrays.fill(dp, max);
            dp[0] = 0;
    
            for (int coin : coins) {
                for (int j = coin; j <= amount; j++) {
                    dp[j] = Math.min(dp[j], dp[j - coin] + 1);
                }
            }
    
            // 可视化 dp 表格
            System.out.println("动态规划表格:");
            for (int j = 0; j <= amount; j++) {
                System.out.print((dp[j] == max ? "∞" : dp[j]) + "\t");
            }
            System.out.println();
    
            return dp[amount] > amount ? -1 : dp[amount];
        }
    }
    
    表格记录数据变化
    金额01234567891011
    最少硬币数011221223323
  1. 分割等和子集

    • 问题描述:给定一个非负整数数组,判断是否可以将这个数组分割成两个子集,使得两个子集的和相等。这可以看作是一个0/1背包问题,目标是找到一个子集,使得其和等于数组总和的一半。
状态转移方程解释
  • 状态定义dp[j] 表示是否可以通过选择数组中的若干元素,使得它们的和为 j

  • 状态转移方程dp[j] = dp[j] || dp[j - num]

    • 对于每个元素 num,如果当前容量 j 可以通过之前的组合或者通过加入当前元素来实现,则 dp[j]true
    • 该问题可以看作是子集和问题的变种,目标是找到一个子集,其总和等于数组总和的一半。我们通过遍历每个元素,逐步判断在不同容量下是否可以找到相应的子集,从而得出最终结果。
    • 分割等和子集问题的解决方法与子集和问题类似,通过动态规划逐步更新状态,确保每个状态的值基于之前的决策而来。最终,我们能够判断是否存在这样一个子集,使得其和等于目标值。
    public class PartitionEqualSubsetSum {
        public static void main(String[] args) {
            int[] nums = {1, 5, 11, 5};
            boolean result = canPartition(nums);
            System.out.println("是否可以分割为两个等和子集:" + result);
        }
    
        public static boolean canPartition(int[] nums) {
            int sum = Arrays.stream(nums).sum();
            if (sum % 2 != 0) return false;
            int target = sum / 2;
    
            boolean[] dp = new boolean[target + 1];
            dp[0] = true;
    
            for (int num : nums) {
                for (int j = target; j >= num; j--) {
                    dp[j] = dp[j] || dp[j - num];
                }
            }
    
            // 可视化 dp 表格
            System.out.println("动态规划表格:");
            for (int j = 0; j <= target; j++) {
                System.out.print((dp[j] ? 1 : 0) + "\t");
            }
            System.out.println();
    
            return dp[target];
        }
    }
    
    表格记录数据变化
    子集和01234567891011
    是否可达110011010011

总结

  • 0/1 背包问题:每个物品只能选一次,使用二维数组保存状态。
  • 完全背包问题:每个物品可以选多次,优化空间复杂度到一维数组。
  • 多重背包问题:每个物品的数量有限,加入多重循环。
  • 分数背包问题:物品可以部分选择,使用贪心算法。
  • 背包问题的其他应用:子集和问题、找零问题、分割等和子集、工作选择问题等都可以通过背包问题的思路来解决。

每种问题都有各自的特点和适用场景,根据具体需求可以选择合适的算法。每个示例中都有代码和可视化部分帮助你理解动态规划表格的变化过程。希望这能帮助你理解不同类型的背包问题。


http://www.kler.cn/a/378169.html

相关文章:

  • ECharts关系图-关系图11,附视频讲解与代码下载
  • 【C++读写.xlsx文件】OpenXLSX开源库在 Ubuntu 18.04 的编译、交叉编译与使用教程
  • 登山第十六梯:深度恢复——解决机器人近视问题
  • 【优选算法---归并排序衍生题目】剑指offer51---数组中的逆序对、计算右侧小于当前元素的个数、翻转对
  • 解决Apache/2.4.39 (Win64) PHP/7.2.18 Server at localhost Port 80问题
  • linux springboot项目启动端口被占用 Port 8901 was already in use.
  • Ubuntu 24 配置vsftp
  • SpringBoot抗疫物资管理与数据分析系统
  • vulhub之Spring篇
  • AIGC与虚拟现实(VR)的结合与应用前景
  • 人工智能中FOPL语言是什么?
  • php实现sl651水文规约解析
  • 自定义view实现历史记录流式布局
  • 5G基础知识
  • 【51 Pandas+Pyecharts | 深圳市共享单车数据分析可视化】
  • Java爬虫:京东商品SKU信息的“窃听风云”
  • 消息中间件类型介绍
  • 共创一带一路经济体,土耳其海外媒体软文发稿 - 媒体宣发报道
  • nodejs入门教程9:nodejs Buffer
  • Vue学习笔记(十一)
  • Unity的gRPC使用之实现客户端
  • 基于统计方法的语言模型
  • kubesphere jenkins自动重定向 http://ks-apiserver:30880/oauth/authorize
  • 开源库 FloatingActionButton
  • new/delete和malloc()/free()的区别及其使用
  • 无人机航拍铁路障碍物识别图像分割系统:创新焦点发布