文章目录:
  1. 01背包
  2. 完全背包
  3. 多重背包问题
本章主要介绍动态规划的常用思路,并介绍了背包问题里面的01背包、多重背包的解题思路和代码

01背包

01背包问题为:给定背包总体积,给N个物品,每个物品有一个体积和重量,只能用一次或不用,问最大重量是多少?
下面是思路,定义函数f(i,j)代表:仅包含前i个物品且总体积小于等于j的背包的最大价值是多少,故最终需要的结果是f(N,V)
初始时把所有值都设为0,且i或j等于0的本身值就为0。
对于一个新的状态f(i,j),可能的值有下面两种可能,如果包含i,则答案为f(i-1,j-vi) + wi;如果不包含i,则为f(i-1,j)。这两个值取最大值就可以算出f(i,j)

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

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 <= 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;
}

完全背包

完全背包的区别是每个物品可以选0-无数次,同样求最大总重量。
思路: 此时DP数组的含义不变。在每次选择f(i,j)时,同样也变成了k+1种选择(选择0,1,2,…,k个第i个元素,k由循环判断当前的j能否大于或等于k个i的价值,即k*wi <= j),循环判断出其中的最大值,就可以算出新的f[i,j]
Screenshot_20250822_221647_com.quark.browser.jpg
代码:

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

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++){
            for(int k = 0; k*v[i] <= j ; k ++)
                f[i][j] = max(f[i][j], f[i-1][j-v[i]*k + w[i]*k]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

但明显能看出来此时使用了三个循环,此时时间复杂度较高,应考虑如何快速计算选0-k次中哪个结果最优,需要优化算法。下面是优化思路:
因为循环的嵌套是从i: 1-N, j: i-V计算的,所以在计算f[i,j]时,f[i, j-v]必然被算过(v代表i的体积,w代表i的重量)。f[i,j]f[i, j-v]之间存在如下的联系:

$$ f[i,j] = Max(f[i-1,j], f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w, ……) $$

$$ f[i,j-v] = Max(f[i-1,j-v],f[i-1,j-2v]+w,f[i-1,j-3v]+2w, ……) $$

那么可以看出两者的关系为

$$f[i,j] = Max(f[i-1,j], f[i,j-v]+w)$$
则此时减少了一层循环,提高运行效率。
新的代码:

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];

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][j-v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

进一步优化:可以把数组转化为一维数组,降低空间复杂度。因为每次计算新的f(i,j)只有第i-1行的当前元素和第i行的j以前元素有用,所以只用一个数组存f(i,j)不会出任何错,只保留j的参数,每次更新直接覆盖对应的j。
代码如下,其实就把上面的代码的f的所有第一个参数删掉:

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];

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[j] = f[j]; 这行默认完成了,直接删掉
            if (j >= v[i]) f[j] = max(f[j], f[j-v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

多重背包问题

多重背包问题是每种物品的个数有限制,基础暴力做法和完全背包问题的暴力做法几乎一样,只是累积的上限不一样,即固定的个数限制。
但是在考虑相同的优化算法时,发现根据f[i,j-v]无法求得f[i,j]的最大值,因为若最大值等于其最后一项,则无法知道前面的最大值,而需要重新计算。(如下图所示)
Screenshot_20250822_233424_com.quark.browser.jpg
此时需要新的计算方法,先需要理解下面的数学原理:
对于_202508241345_25128 1.jpg
则最终的代码:
Screenshot_20250824_140105_com.quark.browser_edit_195279027111346.jpg
每组物品读入之后,在22-29行按照上面的数学原理拆分为2的幂次,用cnt记录现在的添加物品数量(最后一组直接使用s,而不是下一个2的幂次)。然后转化为01背包问题直接计算即可。