背包问题(一)

背包问题详解:01与完全背包

一.01背包

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

0<N,V≤10000
0<vi,wi≤1000

(一).详细分析“01背包”动态规划思路

1.问题本质

你有一个容量有限的背包,和一堆物品。每个物品有自己的体积和价值。你的目标是:在不超过背包容量的前提下,选择一些物品装入背包,使得这些物品的总价值最大

关键在于“0-1”:每个物品只有两种命运,要么被选中 (1),要么不被选中 (0)。不能只装入一个物品的一部分。

2.动态规划(DP)的核心思想

动态规划的精髓在于将复杂问题分解为更小的、重叠的子问题,并通过存储子问题的解(记忆化)来避免重复计算,最终构建出原问题的解。

对于背包问题,我们定义的状态(子问题)是:
dp[i][j] 表示一个状态:只考虑前 i 个物品,在背包容量为 j 的情况下,可以获得的最大价值。

i 的范围是 0 到 N(物品编号从1开始,i=0表示不考虑任何物品)。

j 的范围是 0 到 V(背包容量)。

我们的最终目标就是求出 dp[N][V] 的值。

3.状态转移方程(最关键的一步)

1.不选第i个物品:

如果我们决定不拿这个物品,那么情况就变得非常简单。

此时的最大价值,就等于只考虑前 i-1 个物品,且背包容量仍然为 j 时的最大价值。

也就是 dp[i][j] = dp[i-1][j]

2.选择第i个物品:

如果我们决定拿这个物品,那么首先有一个前提条件:背包的剩余容量必须至少能装下这个物品,即 j >= v[i]

如果我们拿了这个物品,我们会获得它的价值 w[i],但同时也会消耗掉 v[i] 的背包容量。

那么,剩下的背包容量就是 j - v[i]。在这些剩余的容量里,我们依然要做出最优决策。“只考虑前 i-1 个物品,背包容量为 j - v[i] 时的最大价值” 就是我们在剩余容量上能获得的最大价值,即 dp[i-1][j - v[i]]

所以,选择第 i 个物品带来的总价值是:w[i] + dp[i-1][j - v[i]]

结论:我们的目标是最大化价值。因此,对于每一种状态 (i, j),我们都在上述两种选择中挑一个更好的(价值更大的)。

综合起来就是:

如果 j < v[i] (当前背包容量装不下第i个物品):
    dp[i][j] = dp[i-1][j] // 只能不选
否则 (可以装下,进行决策):
    dp[i][j] = max( dp[i-1][j],         // 不选第i个物品
                    w[i] + dp[i-1][j - v[i]]  // 选择第i个物品
                  )

4.初始化

我们需要一个初始状态来开始整个递推过程。

dp[0][0..V]:考虑前 0 个物品(也就是没有任何物品可选)。那么无论背包容量有多大,能获得的最大价值都是 0

同样,dp[0..N][0]:背包容量为 0。那么无论有多少物品,你都装不下任何东西,最大价值也是 0

在实际代码中,我们通常会创建一个大小为 (N+1) x (V+1) 的二维数组,并将第一行 (i=0) 和第一列 (j=0) 全部初始化为 0

5.计算顺序与填表

有了初始状态和状态转移方程,我们就可以想填表一样计算出所有状态的值。

1.外层循环 i 从 1 到 N(逐个考虑每个物品)。

2.内层循环 j 从 0 到 V(逐个考虑每种可能的背包容量)。

3.对于每一个 (i, j),根据上面的状态转移方程,利用之前已经计算好的 dp[i-1][...] 的值来计算 dp[i][j]

最终,表格最右下角的值 dp[N][V] 就是我们要求的答案。

6.空间优化(滚动数组或一维数组)

仔细观察状态转移方程,你会发现:在计算 dp[i] 这一行的所有状态时,只依赖于上一行 dp[i-1] 的数据,而更早的数据就不再需要了。

这意味着我们并不需要存储整个 N x V 的二维数组。我们可以使用一个一维数组 dp[0..V] 来滚动更新。

这个一维数组 dp[j] 在计算过程中,实际表示的是上一轮(i-1)的结果,即 dp[i-1][j]

当我们计算新的一轮 i 时,我们想要用上一轮的数据来覆盖它,得到 dp[i][j]

但是,这里有一个关键点:内层循环 j 必须从大到小(从 V 到 0)进行遍历

为什么?
因为状态转移方程需要的是 dp[i-1][j - v[i]],也就是上一轮、更小容量的状态。如果我们从小到大遍历 j,那么当我们计算 dp[j] 时,dp[j - v[i]] 可能已经在本轮被更新过了(变成了 dp[i][j - v[i]]),这就污染了我们需要使用的“上一轮”的数据。而从大到小遍历可以保证我们使用的 dp[j - v[i]] 仍然是上一轮的结果。

优化后的状态转移(一维数组):

for (int i = 1; i <= N; i++) {
    for (int j = V; j >= v[i]; j--) { // 逆序更新容量
        dp[j] = max(dp[j], w[i] + dp[j - v[i]]);
    }
    // j < v[i] 时,dp[j] 保留上一轮的值
}
 

(二).代码展示

二维数组:
#include <iostream>
#include <algorithm>
using namespace std;

const int MAX_N = 1010;
const int MAX_V = 1010;

int main() {
    int N, V;
    cin >> N >> V;
    
    // 物品数组,下标从1开始使用
    int v[MAX_N] = {0}; // 体积
    int w[MAX_N] = {0}; // 价值
    
    // 读取物品信息
    for (int i = 1; i <= N; i++) {
        cin >> v[i] >> w[i];
    }
    
    // DP数组:dp[i][j]表示考虑前i个物品,背包容量为j时的最大价值
    int dp[MAX_N][MAX_V] = {0};
    
    // 动态规划过程
    for (int i = 1; i <= N; i++) {          // 遍历每个物品
        for (int j = 0; j <= V; j++) {      // 遍历每种容量
            // 默认不选第i个物品
            dp[i][j] = dp[i - 1][j];
            
            // 如果当前容量可以装下第i个物品,考虑选择它
            if (j >= v[i]) {
                dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
            }
        }
    }
    
    // 输出结果:考虑前N个物品,容量为V时的最大价值
    cout << dp[N][V] << endl;
    
    return 0;
}
 
一维数组:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main() {
    int n, v;
    cin >> n >> v;
    vector<int> dp(v + 1, 0);

    for (int i = 0; i < n; ++i) {
        int vi, wi;
        cin >> vi >> wi;
        for (int j = v; j >= vi; --j) {
            dp[j] = max(dp[j], dp[j - vi] + wi);
        }
    }
    cout << dp[v];
}
 

二.完全背包

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

0<N,V≤10000
0<vi,wi≤1000

(一).问题本质

与0-1背包不同,完全背包中每种物品都有无限件可用。这意味着对于每种物品,你可以选择拿0件、1件、2件...直到背包容量装不下为止。

你的目标依然是:在不超过背包容量的前提下,选择物品(数量不限),使得总价值最大

(二).动态规划思路

依然使用动态规划,定义的状态是:
dp[i][j] 表示考虑前 i 种物品,在背包容量为 j 的情况下,可以获得的最大价值。

1.状态转移方程分析

对于第 i 种物品,我们现在有多种选择:可以拿0件、1件、2件...k件(只要不超过背包容量)。

因此,状态转移方程需要遍历所有可能的数量:

dp[i][j] = max{ 
    dp[i-1][j],                          // 不拿第i种物品
    dp[i-1][j - v[i]] + w[i],            // 拿1件第i种物品
    dp[i-1][j - 2*v[i]] + 2*w[i],        // 拿2件第i种物品
    ...
    dp[i-1][j - k*v[i]] + k*w[i]         // 拿k件第i种物品
}
其中 k 满足:k * v[i] <= j

2.优化状态转移方程

上面的方程需要遍历k,时间复杂度较高(O(NVV))。可以通过数学变换来优化:

观察 dp[i][j] 和 dp[i][j - v[i]] 的关系:

dp[i][j] = max( dp[i-1][j], dp[i-1][j-v[i]] + w[i], dp[i-1][j-2v[i]] + 2w[i], ... )

dp[i][j - v[i]] = max( dp[i-1][j-v[i]], dp[i-1][j-2v[i]] + w[i], dp[i-1][j-3v[i]] + 2w[i], ... )

可以发现:dp[i][j] = max( dp[i-1][j], dp[i][j - v[i]] + w[i] )

这个优化后的方程极其重要! 它的含义是:

要么不选第i种物品:dp[i-1][j]

要么至少选一件第i种物品:dp[i][j - v[i]] + w[i]

注意这里第二项是 dp[i][j - v[i]] 而不是 dp[i-1][j - v[i]],这是因为我们允许重复选择同一物品。

3.最终的状态转移方程

如果 j < v[i]:
    dp[i][j] = dp[i-1][j]
否则:
    dp[i][j] = max( dp[i-1][j], dp[i][j - v[i]] + w[i] )

4.初始化

与0-1背包相同:

dp[0][0..V] = 0(没有物品可选)

dp[0..N][0] = 0(没有容量可用)

5.空间优化(一维数组)

同样可以优化空间,使用一维数组 dp[j]。但注意:这里内层循环需要从小到大遍历

为什么?
因为状态转移方程是 dp[i][j] = max(dp[i-1][j], dp[i][j - v[i]] + w[i]),我们需要的是本轮的 dp[i][j - v[i]],所以应该先计算小的j,再计算大的j。

优化后的代码结构:

for (int i = 1; i <= N; i++) {
    for (int j = v[i]; j <= V; j++) { // 从小到大遍历
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    }
}

(三).代码展示

二维数组:

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, v;
    cin >> n >> v;
    vector<int> vi(n + 1, 0);
    vector<int> wi(n + 1, 0);
    vector<vector<int>> dp(n + 1, vector<int>(v + 1, 0));

    for (int i = 1; i <= n; i++) cin >> vi[i] >> wi[i];

    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= v; j++) {
            if (j < vi[i]) dp[i][j] = dp[i - 1][j];
            else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - vi[i]] + wi[i]);
            }
        }
    }

    cout << dp[n][v] << endl;
}
 

一维数组:

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, v;
    cin >> n >> v;
    vector<int> vi(n + 1, 0);
    vector<int> wi(n + 1, 0);
    vector<int> dp(v + 1, 0);

    for (int i = 1; i <= n; i++) cin >> vi[i] >> wi[i];

    for (int i = 1; i <= n; i++) {
        for (int j = vi[i]; j <= v; j++) {
            dp[j] = max(dp[j], dp[j - vi[i]] + wi[i]);
        }
    }

    cout << dp[v] << endl;
}
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值