【DP知识点小结】
动态规划系列
背包问题
背包问题是个非常经典的问题,但是其变种非常之多,也非常灵活。
这里结合算法进阶的知识点和ACwing的题目,一起学习和练习相关知识点。
1. 0-1背包问题
问题描述:
给定N个物品,其中第i个物品的体积为,价值为。有一容积为M的背包,要求选择一些物品放入背包中,使得物品总体及不超过M的情况下,物品的价值总和最大。
因为在该题目描述中,不能部分选取一个物品,要么选择一个物品,要么不能选择该物品,所以这样的一种模型也叫做0-1背包问题(0-1蕴含每件物品最多只用一次)。
完全背包问题:每件物品可以选取有无限个;
多重背包问题:每件物品是有限个数;
分组背包问题:有N组,每组只能选择一个;
//动态规划(线性DP)
DP问题主要由两点:
1.状态表示; 表示什么样的集合(集合角度考察),属性(最小,最大,数量等等)
2.状态计算(状态迁移);
3.DP优化,主要是等价形式变换;
集合的选法:怎么确定选取条件;
在本题中,f(i, j) = 只从前i个物品中选取,且总体积小于等于j, 总价值的最大值;
那么根据条件定义,所求解应该是f(N, V):从N个物品中选取,且总体积小于等于V,获得的最大价值;
状态计算:集合划分
f(i, j) = f(不包含i的集合) + f(包含i的集合); //不漏, 重复(有可能不需要)
f(不包含i的集合) :应该是f(i - 1, j), 表示在前i-1个物品中,选取体积<= j的最大价值;
f(包含i的集合):f(i, j), 但因为在此时未知,但是我们可以用之前的已知状态已知来计算。
因为在计算f(i, j), 那么在(i, j)状态之前的状态值都是已知的,因为选取了第i个物品,那么对于f(i - 1, j - Vi)是已知的,
所以f(i, j) = f(i -1, j - Vi) + Wi;
在两者之间取最大值,就是当前状态的计算方式。
note:DP问题也需要确定初始值,也就是初始状态;
DP问题其实可以看成是有限状态机,但是是无后效性的(也就是不能形成环状);
//朴素DP算法
f[0][0] = 0; //初始化起始状态;
for (int i = 1; i <= n ; i++)
for (int j = 1; j <= m; j++) {
f[i][j] = f[i - 1][j]; // 初始化为不带i的情况;
if (j >= v[i]) {
f[i][j] = max(f[i][j], f[i-1][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl; //输出最终结果;
//优化版本的DP算法
因为f(i, x)只用到f(i - 1, xx),所以可以变换为下面:
f[0] = 0; //初始化起始状态;
for (int i = 1; i <= n ; i++)
for (int j = 1; j <= m; j++) {
//f[j] = f[j]; // 删掉i的这一阶; 删掉之后变为恒等式,进而可以省略;
if (j >= v[i]) {
f[j] = max(f[j], f[j - v[i]] + w[i]); ....(1)
}
}
cout << f[m] << endl; //输出最终结果;
//又因为(1)式只有在j >= v[i]的情况下才会成立,当循环从v[i]开始遍历时,就不需要判断条件了;
进化如下:
f[0] = 0; //初始化起始状态;
for (int i = 1; i <= n ; i++)
for (int j = v[i]; j <= m; j++) {
//f[j] = f[j]; // 删掉i的这一阶; 删掉之后变为恒等式,进而可以省略;
//if (j >= v[i]) {
f[j] = max(f[j], f[j - v[i]] + w[i]); ....(1)
//}
}
cout << f[m] << endl; //输出最终结果;
需要注意(1)中式子含义
f[j] = max(f[j], f[j - v[i]] + w[i]);
在此时,等价于f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]), 这里的[j - v[i]]是第i层的状态,之前分析的状态含义应该是第i-1层的才对
所以这么做就是错误的解法。
自习分析一下为啥状态出现错误。
可以尝试枚举模拟一下计算,
在i-1次外循环执行完毕后,那么f[j]中保存的都是第i-1次时计算的状态,
那么在第i次外循环时,
当j = v[i],那么max(f[j],f[j - v[i]] + w[i] = f[0] = w[i]) = f[j] = f[v[i]],此时更新为第i次的状态值
当j = v[i] + 1, f[j] = max(f[j], f[j - v[i]] + w[i]) ,也更新为第i次的状态值;
...
当j = 2v[i], 那么f[j] = max(f[j], f[j - v[i]] + w[i]) = max(f[j], f[v[i]] + w[i])
而f[v[i]]在前面已经更新为第i次的状态了,所以这个状态转移方程就计算错误了。
可以将j从m开始往下遍历,
f[0] = 0; //初始化起始状态;
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]); ....(2)
}
cout << f[m] << endl; //输出最终结果
这么做的含义在哪里,因为如果j从m开始遍历时,对应的f[j - v[i]],不妨可以尝试枚举几个j的值试试
当j = m, f[j - v[i]] = f[m - v[i]] 还是第i-1次时保存的状态,那么就可以得到f[m] = max(f[m], f[m - v[i]] + w[i]);
此时计算完后,f[m]的状态是第i次的,f[1 ~ m-1]还是第i-1次的,
那么同理,可由j = m-1, f[j - v[i]] = f[m - 1 - v[i]] != f[m], 所以其值还是第i-1的值,
所以f[m-1] = max(f[m-1], f[m - 1 - v[i]] + w[i]); 计算过后更新f[m-1]为第i次的状态;
...
j = 2v[i], f[j] = max(f[j], f[j - v[i]] + w[i]) = max(f[j], f[v[i]] + w[i]); 因为前面更新第i次状态时都没有更新到f[v[i]],所以这里
f[v[i]]依旧是第i-1次循环的状态,所以这里f[j] = f[2v[i]]更新为第i次的状态值;
...
j = v[i], f[j] = max(f[j], f[j - v[i]] + w[i]) = max(f[j], f[0] + w[i]);中依旧取得是第i-1次循环状态的值,并更新f[v[i]]的状态值;
从而可以得到其没有破坏状态转移方程的值。
最终优化的结果如下:
f[0] = 0; //初始化起始状态;
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]); ....(1)
}
cout << f[m] << endl; //输出最终结果;
[Acwing02]:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, V;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> V;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
//初始化状态
f[0] = 0; //全局变量一般默认为零初始化;
for (int i = 1; i <= n; i ++ ) {
for (int j = V; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
cout << f[V] << endl;
return 0;
}
这里还有一个有意思的知识点,介于朴素DP和一维优化DP之间,就是滚动数组的优化方式,降低空间开销。
//滚动数组优化方式
const int MAX_N = 100010;
int f[2][MAX_N];
int main()
{
//初始化初始状态;
memset(f, 0xcf, sizeof f);
//设置起始状态;
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
{
f[ i & 1][j] = f[(i - 1) & 1][j];
}
for (int j = v[i]; j <= m; j++)
{
f[i & 1][j] = max(f[i & 1][j], f[(i-1) & 1][j - v[i]] + w[i]);
}
}
int ans = 0;
for (int j = 0; j <= m; j++) {
ans = max(ans, f[n & 1][j]);
}
}
//可以这么做的原因在于,把第i次循环的状态存储在第一维下标为i&1的二维数组中,当i为技术, i&1 = 1, 当i为偶数, i&1 = 0.
//因此,两个状态在f[0][]和f[1][]两个数组中交替使用,空间复杂度使用为O(m);
2.完全背包问题
问题描述:
给定N种物品,其中第i种物品的体积为,价值为,并且有无数个。有一个容积为M的背包,要求选择若干个物品放入背包中,使得物品总体积不超过M的前提下,物品的价值总和最大。
分析:
这个模型描述和0-1背包问题类似,但是其物品可以任意选择;
那么怎么确定状态选择呢?
这个因为和0-1背包问题类似,不妨可以选择状态为:前i个物品,总体积不大于j的所有选法;
属性:求Max;
状态转移方程:
0-1背包问题,状态划分为选i和不选i两种情况,
我们在完全背包也可以这么来做,
分别表示对第i个物品选0个, 选1个, 选2个,...,
那么上限是多少呢?不妨假设上限为k;
那么f[i][j] = max(f[ i - 1][j], f[i - 1][j - k*v[i]] + k*w[i]);
这里f[i - 1][j]表示对物品i选择0个, 后面的f[i - 1][j - k*v[i]] + k*w[i],表示选择物品i共k个;
所以: j - k* v[i] >= 0 => k <= j/v[i];
那么对于朴素DP的代码为:
#include <iostream>
#include <cstring>
#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];
}
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = 0; k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
//优化
因为
f[i][j] = Max(f[i-1][j], f[i-1][j - v[i]] + w, f[i-1][j - 2v[i]] + 2w, ...,); ... (1)
f[i][j - v[i]] = Max(f[i-1][j - v[i]], f[i-1][j - 2v[i]] + 2*w, ...) ... (2)
可以发现(1)和(2)后面部分相似;
所以f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w); //注意这里,转移的是第i次的循环状态转移的, 0-1背包转移的是i-1转移的;
#include <iostream>
#include <cstring>
#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];
}
f[0][0] = 0;
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;
}
//继续优化
//可仿照0-1背包变为一维;
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= m; j++) { //注意顺序,这里因为要求的是第i次循环状态,所以需要从小到大的遍历;参考之前0-1背包的枚举推断;
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
3.多重背包问题
问题描述:
给定N种物品,其中第i种物品的体积为,价值为,并且有个。有容积为M的背包,要求选择若干物品放入背包,使得物品总体积不超过M的前提下,物品的价值总和最大。
[分析]:
按照之前的0-1背包问题,定义问题状态:
f[i][j]:所有只从前i个物品中选择,并且总体积不超过j的选法;
属性:Max;
状态转移:
f[i][j]:可以选物品i 0个, 1个, 2个, ...., Ci个;
所以状态转移方程
f[i][j] = max(f[i-1][j - k*v[i]] + k*w[i]), k = 0, 1,2, ..., Ci;
//朴素多重背包问题,和完全背包问题一样的;
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N= 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
cin >> n >>m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)
{
for (int k = 0; k <= s[i] && k*v[i] <= j; k++)
{
f[i][j] = max(f[i][j], f[i-1][j - k*v[i]] + k* w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
优化:
f[i, j] = max(f[i-1, j], f[i-1][j-v]+w, f[i-1][j-2v]+2w,..., f[i-1][j-kv]+kw);
f[i, j - v] = max( f[i-1][j-v] , f[i-1][j-2v]+w ,..., f[i-1][j-kv]+(k-1)w, f[i-1][j-(k+1)v] + kw);
可以看见很多项还是重复的,除了首尾两项的差异。
但因为是求max,无法直接做减法。
这里采用二进制拆分法来做:
从2^0, 2^1, 2^2, ..., 2^(k-1)这k个2的次幂中选出若干个相加,可以得到范围在[1, 2^k - 1]的数。
进一步,求出满足2^0+2^1 +2^2 + ... + 2^p <= Ci的最大正整数p。
设Ri = Ci - 2^0 - 2^1 -... - 2^p,那么:
1.根据p的最大型,有2^0 + 2^1 + ... + 2^(p+1) > Ci, 可以得到2^(p+1) > Ri ,因此可以在2^0, 2^1, ..., 2^p中选出若干个相加可以表示出0-Ri之间的任何整数;
2.从2^0, 2^1, ..., 2^p 及Ri中选取若干个相加,可以表示出Ri ~ Ri + 2^(p+1) - 1范围内的数,而又因为Ri的定义
Ri + 2^(p+1) - 1 = Ci - 2^0 - 2^1 -... - 2^p + 2^(p+1) - 1 = Ci,因此从2^0, 2^1,..., 2^p, Ri中选取出来的若干数可以表示出Ri到Ci之间的任何整数。
综上所述,可以将数量为Ci的第i中物品拆分成p+2个物品,它们的体积分别为:
2^0 *vi , 2^1 * vi, ..., 2^p * vi, Ri *vi,
这p+2个物品可以凑成0~Ci*vi之间所有被vi整除的数,并且不能凑成大于Ci * vi的数。这样就可将原问题转换为体积为Vi的物品可以使用0~Ci次,该方法把每种物品拆分成O(logCi)组, 对这些新的物品组做一次0-1背包问题即可。
本来朴素完全背包问题时间复杂度为O(N * V *S), 现在变为了O(N*V* logS), S越大优化效果越好。
//二进制拆分法优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 25000, M = 2010;
int n,m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i++)
{
int a, b, s;
cin >> a>>b >>s;
int k = 1;
while (k <= s) {
cnt++;
v[cnt] = a*k;
w[cnt] = b*k;
s -= k;
k *= 2;
}
if (s > 0) {
cnt++;
v[cnt] = a*s;
w[cnt] = b*s;
}
}
n = cnt;
//执行0-1背包问题
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;
}
4.分组背包问题
问题描述:
给定N组物品,其中第i组物品有Ci个物品。第i组的的第j个物品的体积为Vij,价值为Wij。有一容积为M的背包,要求选择若干物品进入背包,是的每组至多选择一个物品并且物品总体积不超过M的前提下,物品的价值总和最大。
[分析]:
状态表示:只从前i组物品中选,且总体积不大于j的所有选法;
属性:Max;
状态转移:
f[i, j]:
1.不从第i组选择物品,f[i -1, j];
2.从第i组选取物品,f[i - 1, j - vij] + wij
因此, f[i, j] = max(F[i - 1, j], f[i - 1 , j - vij] + wij), j = 1, 2, ..., k;
//朴素算法:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main()
{
cin >> n >>m;
for (int i = 1; i <= n; i++)
{
cin>>s[i];
for (int j = 0; j < s[i]; j++)
cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = 0; k < s[i]; k++)
{
if (v[i][k] <= j)
{
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
}
cout << f[m] << endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】