各种背包问题简述

发布于:2025-09-07 ⋅ 阅读:(18) ⋅ 点赞:(0)
什么是背包问题?

背包问题(Knapsack Problem)是计算机科学和运筹学中的一个经典组合优化问题。给定一组物品,每个物品都有其特定的体积(或重量)和价值,在不超过背包总容量的情况下,如何选择物品装入背包,使得背包中物品的总价值最大化。(在学习背包问题之前,笔者建议先阅读笔者写于之前的动态规划入门:从记忆化搜索到动态规划以便更容易理解背包问题)

背包问题的多种变体:

01背包问题:每种物品只能选或不选(0次或者1次)
完全背包问题:每种物品可以选择无数次
多重背包问题:每种物品有数量限制
分组背包问题:物品被分成若干组,每组只能选一个物品
混合背包:以上四种背包问题混在一起
多维费用的背包问题:限制条件不只有体积,还会有其他因素(比如重量)
下面我将借助一些例题带读者深入理解各种背包问题:

01背包:

题目描述:有一个容量为 V 的背包和 N 件物品,每件物品有一个体积 v[i] 和一个价值 w[i]。要求从这 N 件物品中选择若干件放入背包,使得背包中物品的总体积不超过 V,且总价值最大。每件物品只能选择一次(即不能重复选择)。

看到这个问题,相信很多同学第一反应是利用贪心策略,但贪心有三种不同的情况,分别是:

  1. 尽可能选价值较大的物品
  2. 尽可能选提交较小的物品
  3. 尽可能选 w/v(价值 / 体积)较大的物品

但这三种策略都是可以证明为否的,且很容易举出反例,因此贪心策略不可行。
这时我们就可以考虑利用动态规划来解决这个问题,背包问题究其本质也是动态规划的一种,因此,我们可以用动态规划同款的方式来解决问题:
在体积不超过 j 的情况下,从[1, i]区间挑选物品,我们可以选择是否选择第 i 号物品,如果不选择,那么它的结果与从[1, i - 1]区间挑选物品结果一致,即f[i][j] = f[i - 1][j],如果选择第 i 个元素,那么之前位置所选的物品体积总合不能超过 j - v[i],即,f[i][j] = w[i] + f[i-1][j - v[i]]
算法原理

  1. 状态表示:f[i][j]表示:在 [1,i] 区间挑选物品,总体积不能超过 j,所有选法下,最大的价值
  2. 状态转移方程:f[i][j] = max(f[i - 1][j], f[i-1][j - v[i]] + w[i])(如果j-v[i] >= 0)
  3. 初始化:将第一行全部初始化为0
  4. 填表顺序:从上往下每一行
  5. 最终结果:f[n][v]

代码实现:

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> v[i] >> w[i];

	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j <= m; j++)
		{
			f[i][j] = f[i - 1][j];
			if (j >= v[i])
			{
				f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
			}
		}
	}
	cout << f[n][m] << endl;

	return 0;
}

空间优化:可以将 f 数组从二维降为一维。由于每次更新时都是顺序处理,且使用过的旧值不会被重复利用,因此可以不断覆盖原数组,仅保留一维数组来记录最终结果。可以将 f 数组从二维降为一维。由于每次更新时都是从右往左顺序处理,且使用过的旧值不会被重复利用,因此可以不断覆盖原数组,仅保留一维数组来记录最终结果。

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> v[i] >> w[i];

	for (int i = 1; i <= n; i++)
	{
		for (int j = m; j >= v[i]; j--)//修改遍历顺序
		{
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}
	
	cout << f[m] << endl;

	return 0;
}

感觉看明白了的可以移步洛谷P1048采药自行检测。

完全背包:

题目描述:给定一个容量为 V 的背包和 N 种物品,每种物品有无限个可用。第 i 种物品的体积为 v[i],价值为 w[i]。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出这个最大总价值。
同样的,贪心策略同样易证为伪,这里便不再赘述,此时,其他量皆与01背包相同,所以这里主要推导状态转移方程。
在总体积不超过 j 的情况下,从[1, i]区间挑选物品,我们可以选择是否选择第 i 号物品,以及选择几个 i 号物品,当不选择第 i 号物品时 f[i][j] = f[i - 1][j],与前 i - 1 号保持一致。当我们选择一个时 f[i][j] = f[i-1][j-v[i]] + w[i],选择两个时 f[i][j] = f[i-1][j-2 * v[i]] + 2 * w[i],…,依次类推,选择 k 个时 f[i][j] = f[i-1][j-k * v[i]] + k * w[i]。所以,f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i],......,f[i-1][j-k * v[i]] + k * w[i])。但是这样,我们依然不能很直观的看出状态转移方程是什么样子的,那我们在往下看一层,即 f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j-2 * v[i]] + w[i],......,f[i-1][j-k * v[i]] + (k-1) * w[i])。这时我们可以发现,f[i][j-v[i]]中每一个元素加上 w[i] 就可以和 f[i][j] 中的元素对应上,因此,f[i][j] 中除了 f[i-1][j] 意外所有元素取max即为 f[i][j-v[i]] + w[i]。所以,我们可以明确推导出f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])。(这种技巧不一定所有的方式都适用,但当看见元素样子差不多时可以尝试这样的方式)
算法原理

  1. 状态表示:f[i][j]表示:从[1,i]区间中挑选,总体积不超过j的情况下,所有选法下,最大价值
  2. 状态转移方程:f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])
  3. 初始化:f[0][i] = 0
  4. 填表顺序:从上往下,每一行从左往右
  5. 最终结果:f[n][v]

代码实现

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> v[i] >> w[i];

	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			f[i][j] = f[i - 1][j];
			if (j > v[i])f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
		}
	}
	cout << f[n][m] << endl;

空间优化

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> v[i] >> w[i];

	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			f[j] = max(f[j], f[j - v[i]] + w[i]);
		}
	}
	cout << f[m] << endl;

感觉看明白了的可以移步洛谷P1616疯狂的采药自行检测。

空间优化小结:

在空间优化下
01背包:第二层 for 循环改为从大到小 从右到左
完全背包:第二层for循环依然为从小到大 从左往右

多重背包:

题目描述:有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 x[i] 件可用,每件体积是 v,价值是 w。求解将哪些物品装入背包可使这些物品的总体积不超过背包容量,且总价值最大。
由于多重背包的状态转移方程分析部分与完全背包完全相同,因此在这里便不过多赘述。即f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j-2 * v[i]] + w[i],......,f[i-1][j-k * v[i]] + (k-1) * w[i])。由于每种物品的可用件数不一致,因此不能像我完全背包一样做优化,直接用一个状态来表示若干内容,需要一次一次挨个比对。
算法原理

  1. 状态表示:f[i][j]表示:从[1,i]区间中挑选,总体积不超过j的情况下,所有选法下,最大价值
  2. 状态转移方程:f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])
  3. 初始化:f[0][i] = 0
  4. 填表顺序:从上往下,每一行从左往右
  5. 最终结果:f[n][v]

代码实现

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> x[i] >> w[i] >> v[i];

	for (int i = 1; i <= n; i++)
		for (int j = m; j >= 0; j--)
			for (int k = 0; k <= x[i] && k * w[i] <= j; k++)
				f[i][j] = max(f[i][j], f[i - 1][j - k * w[i]] + k * v[i]);

	cout << f[n][m] << endl;
	return 0;
}
分组背包

题目描述:有 N 组物品和一个容量为 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 v,价值是 w。求解将哪些物品装入背包可使这些物品的总体积不超过背包容量,且总价值最大。
算法原理

  1. 状态表示:f[i][j]表示:从前i个小组中挑选,总重量不超过j的情况下,最大价值
  2. 状态转移方程:f[i][j] = max(f[i-1][j], f[i-1][j - v[k]] + w[k]),其中 k 是第 i 组中的物品。
  3. 初始化:全部为0
  4. 填表顺序:从上往下 每一行从左往右
  5. 结果:f[n][v]

代码实现

int main() {
    int n, V;
    cin >> n >> V;
    vector<vector<pair<int, int>>> groups(n + 1); // 每组物品的v和w
    vector<vector<int>> f(n + 1, vector<int>(V + 1, 0));

    for (int i = 1; i <= n; i++) {
        int s;
        cin >> s; // 第i组的物品数量
        while (s--) {
            int v, w;
            cin >> v >> w;
            groups[i].push_back({v, w});
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = V; j >= 0; j--) {
            f[i][j] = f[i - 1][j]; // 不选第i组的任何物品
            for (auto item : groups[i]) {
                if (j >= item.first) {
                    f[i][j] = max(f[i][j], f[i - 1][j - item.first] + item.second);
                }
            }
        }
    }

    cout << f[n][V] << endl;
    return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到