- hzoi013 的博客
动态规划:背包DP
- 2025-2-6 17:29:15 @
- 01背包:(无下限的术式)
例题:采药
Description
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。
医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
Input Format
输入的第一行有两个整数 T(1 <= T <= 1000)T(1<=T<=1000) 和 n(1<=n<=100) ,用一个空格隔开,TT 代表总共能够用来采药的时 间,nn代表山洞里的草药的数目。
接下来的 nn 行每行两个整数 t,c,(1≤t,c≤100),分别表示采摘某 株草药的时间和这株草药的价值。
Output Format
输出包括一行,这一行只包含一个整数,表示在规定的时间内,可以
采到的草药的最大总价值。
Sample 样例输入
70 3
71 100
69 1
1 2
样例输出
3
分析: 试求在背包有一定限制的情况下,利用物品价值特性所能放入的最大值。
符合01背包的特点,每次有两种选择,即选择物品或不选。
采用压维后的一维数组f[j]存储背包容量为j是所能取的最大值。
状态转移方程:
例题代码:
#include <bits/stdc++.h>
using namespace std;
int T,n,t[1005],c[1005],f[1005];//f[i][j]表示前i件草药放进时间为j的背包的最大价值
int main()
{
cin >> T >> n;
for (int i = 1; i <= n; i++) cin >> t[i] >> c[i];
for (int i = 1; i <= n; i++)//从取1件到取n件
{
for (int j = T; j >= t[i]; j--)//枚举每一t
{
f[j] = max(f[j],f[j-t[i]]+c[i]);
}
}
cout << f[T] << endl;
return 0;
}
-完全背包:(无上限的术式)
1.朴素版本
例题:完全背包问题
Description
设有 n 种物品,每种物品有一个重量及一个价值。
但每种物品的数量是无限的,同时有一个背包,最大载重量为 M。
请从 n 种物品中选取若干件 (同一种物品可以多次选取),
使其重量的和小于等于 MM,而价值的和为最大。
Input Format
第一行:两个整数,M (背包容量,M<=200 )和 N (物品数量,N<=30 );
第 2..N+1 行:每行二个整数 Wi,Ci,表示每个物品的重量和价值。
Output Format
仅一行,一个数,表示最大总价值。
Sample
样例输入
10 4
2 1
3 3
4 5
7 9
样例输出
max=12
分析:完全背包之所以有异与01背包,是因为它具有自身
的选择独特性,即每种物品的选择数量没有上限,(但会因背
包容量的限制受限)。
具体的,例如01背包进行dp时,因为有压栈,所以需要使内层循环
逆序遍历,这样才能使上次dp后的状态保留下来,不会发生改变。
完全背包则不然。当进入其内层循环时,因为可选的数量没有限制。
所以需要使内层循环正序遍历,因为其保留了当前状态下所能取得的
最大值,所以f[j]之前的数都会对上一次的状态发生一定改变,
对应的f[j]也会在其基础上进行选择,从而完成了dp过程。
例题代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,w[35],c[35],f[35][205];
int main()
{
cin >> m >> n;
for (int i = 1; i <= n; i++) cin >> w[i] >> c[i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
for (int k = 0; k*w[i] <= j; k++)
f[i][j] = max(f[i-1][j],f[i-1][j-k*w[i]]+k*c[i]);
cout << "max=" << f[n][m] << endl;
return 0;
}
2.二进制拆分
原理:任何数字都可以表示成若干个2的k次方数字的和,例如7可
以用2º+2¹+2²表示;这很好理解,因为任何正数都可以转成二进
制,二进制就是若干个“1”(2的幂数)之和。
基于以上原理,所以在此,我们把完全背包的模型进行优化,
即每次不会使f[j]只加c[i],而是加2的k次方乘c[i]。使时间复杂度降为O(n*log(V/v[i])).
代码实现:
for (int i = 1; i <= 1; i++)
for (int k = 1; k <= V/v[i]; k<<=1)//等于k*=2
for (int j = V; j >= k*v[i]; j--)
f[j] = max(f[j],f[j-k*v[i]]+k*c[i]);
- 多重背包
1.朴素版本
例题:庆功会
Description 为了庆贺班级在校运动会上取得全校第一名成绩,
班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。
期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。
Input Format
第一行二个数n(n<=500),m(m<=6000),其中n代表希望购买的奖品的种数,m表示拨款金额。
接下来n行,每行3个数,v、w、s,
分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和购买的数量(买0件到s件均可),
其中v<=100,w<=1000,s<=10。
Output Format
第一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。
Sample
样例输入
5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
样例输出
1040
分析:并无很大改变,只是由完全背包的无上限改为了规定数量。
只需将循环次数降为规定数量即可
例题代码:
#include <bits/stdc++.h>
using namespace std;
int n,m,v[505],w[505],s[505],f[6005];
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 = m; j >= 0; j--)//逆序遍历每一种背包容量(与01背包类似)
for (int k = 0; k*v[i] <= j && k <= s[i]; k++)//枚举每一次选取几个i种物品
f[j] = max(f[j],f[j-k*v[i]]+k*w[i]);
cout << f[m] << endl;
return 0;
}
2.二进制优化
原理与完全背包的二进制拆分一模一样,不再详细介绍。
具体操作:将第i种物品分成若干件物品,其中每件物品有一个系
数,这件物品的体积和价值均是原来的体积和价值乘以这个系数。
使这些系数分别为2º,2¹,2²,...,2的k-1次方,b[i]-2k+1,
且k是满足b[i]-2k+1>0的最大整数。
例如,如果b[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
例题:逃亡的准备
Description
每个人小时候都有自己的理想,但随着时间推移,
渐渐的大多数人的理想变成了金钱、地位、面子... 于是大多数人 就变成了传说中的俗人。
但我们的中中始终有自己的梦想,他要环游世界!
在 20XX 年 X 月 X 日中中开始环游世 界的准备工作。
他开始准备自己的行囊,中中背包的容积为 m ,
中中有 n 样有用的东西 ,每样东西都有自己的价值 Wi ,和体积 Vi,
每一样物品有Ni个 ( Ni=0 时表示有无限多个),
于是乎我们的问题就是(你应该已经猜到了)...中中能带走的 东西的最大价值。
Input Format
第 11 行: N,M --物品的种类和背包的容积
第 2−N+1 行: Vi,Wi,Pi --三个整数:每个物品的体积、价值、个数。
Output Format
单独的一行在给定的限制里可能得到的最大的价值。
Sample
样例输入
5 50
1 1 50
2 4 3
48 49 1
1 51 1
3 3 3
样例输出
106
Hint 30% 数据满足 1 <= m,n <=10001<=m,n<=1000
100% 数据满足 1 <=m,n <= 100001<=m,n<=10000
例题代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n,m,n1,v[MAXN],w[MAXN],p[MAXN],f[MAXN];
bool a[MAXN];
int main()
{
cin >> n >> m;
n1 = 0;
for (int i = 1; i <= n; i++)
{
int x,y,s,t = 1;//每种物品的体积,价值,数量及其二进制优化后分成的指数堆
cin >> x >> y >> s;
if (s == 0)//如果是完全背包
{
v[++n1] = x;//正常赋值
w[n1] = y;
a[n1] = 0;//标记为完全背包
continue;
}
while (s >= t)
{
n1++;//数组数加1
v[n1] = x*t;//存入指数堆代表的物品数量的相应空间和价值
w[n1] = y*t;
a[n1] = 1;//标记为多重背包
s -= t;
t *= 2;//指数加1
}
n1++;
v[n1] = x*s;//存入整体
w[n1] = y*s;
a[n1] = 1;
}
for (int i = 1; i <= n1; i++)
if (a[i])//大于零是多重背包
for (int j = m; j >= v[i]; j--)
f[j] = max(f[j],f[j-v[i]]+w[i]);
else
for (int j = v[i]; j <= m; j++)
f[j] = max(f[j],f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
- 综合练习:混合背包
例题:混合背包
Description
一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,
它们的价值分别为C1,C2,...,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。
求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
Input Format
第一行:二个整数,V(背包容量,V<=200),N(物品数量,N<=30);
第2..N+1行:每行三个整数Wi,Ci,Pi,前两个整数分别表示每个物品的重量,价值,
第三个整数若为0,则说明此物品可以购买无数件,若为其他数字,则为此物品可购买的最多件数(Pi)。
Output Format
仅一行,一个数,表示最大总价值。
Sample
样例输入
10 3
2 1 0
3 3 1
4 5 4
样例输出
11
例题代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int m,n,w[MAXN],c[MAXN],p[MAXN],f[MAXN];
int main()
{
cin >> m >> n;
for (int i = 1; i <= n; i++) cin >> w[i] >> c[i] >> p[i];//该物品的体积,价值和属性(是否为完全背包)
for (int i = 1; i <= n; i++)
if (p[i] == 0)//为完全背包
for (int j = w[i]; j <= m; j++)
f[j] = max(f[j],f[j-w[i]]+c[i]);
else
for (int k = 1; k <= p[i]; k++)//01背包或多重背包
for (int j = m; j >= w[i]; j--)
f[j] = max(f[j],f[j-w[i]]+c[i]);
cout << f[m] << endl;
return 0;
}
- 二维费用背包
先看题:
例题:NASA的食物计划
Description
航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱,每件食品都有各自的体积、质量以及所含卡路里,
在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值,当然每个食品只能使用一次.
Input Format
第一行两个数体积最大值(<400)和质量最大值(<400)
第二行 一个数 食品总数N(<50).
第三行-第3+N行
每行三个数 体积(<400) 质量(<400) 所含卡路里(<500)
Output Format
一个数 所能达到的最大卡路里(int范围内)
Sample
样例输入
320 350
4
160 40 120
80 110 240
220 70 310
40 400 22
样例输出
550
分析:相比较之前的DP来说,背包容量的限制又多了一个维度,其实不必过多考虑,只需是dp数组进行调节,循环次数增加维度即可
费用加了一维,只需状态也加一维即可。设f[i][j][k]表示前i件物品付出两种代价分别为j和k时可获得的最大价值。
状态转移方程就是:$f[i][j][k]=max{f[i-1][j][k],f[i-1][j-a[i]][j-b[i]]+c[i]}$
至于问题是0/1背包、完全背包还是多重背包直接套用一维的方法即可。
例题代码:
#include <bits/stdc++.h>
using namespace std;
int W,V,n,f[405][405],v[55],w[55],c[55];
int main()
{
cin >> V >> W >> n;//体积和质量最大值以及物品种类数
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> c[i];//具体的数值
for (int i = 1; i <= n; i++)//枚举每一种物品
for (int j = V; j >= v[i]; j--)//第一种性质(用01背包重的逆序更新)
for (int k = W; k >= w[i]; k--)//第二种性质
f[j][k] = max(f[j][k],f[j-v[i]][k-w[i]]+c[i]);
cout << f[V][W] << endl;
return 0;
}
- 分组背包
例题:分组背包
Description
一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn。
这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。
求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
Input Format
第一行:三个整数,V(背包容量,V<=200),N(物品数量,N<=30)和T(最大组号,T<=10);
第2..N+1行:每行三个整数Wi,Ci,P,表示每个物品的重量,价值,所属组号。
Output Format
仅一行,一个数,表示最大总价值。
Sample
样例输入
10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3
样例输出
20
分析:分组背包与01背包十分相似,不同之处是分组背包一组内只能选一个,所以在判断时稍加改动即可。
具体的,每组物品要么一件不取,要么只取其中的一件,确实跟0/1背包很类似,0/1背包是对单个物品而言,
而分组背包是对分组而言定义f[i][j]表示第i组物品在背包容量为j对第i组的第k个物品进行决策的最大价值。
动态转移方程为:$f[i][j]=max(f[i-1][j],f[i-1][j-w[g[i][k]]]+c[g[i][k]])$
例题代码:
#include <bits/stdc++.h>
using namespace std;
int V,n,T,w[35],c[35],p[35],g[15][35],f[205];
int main()
{
cin >> V >> n >> T;//背包容量,物品数和组数
for (int i = 1; i <= n; i++)
{
cin >> w[i] >> c[i] >> p[i];//每个物品的重量,价值,所属组号
g[p[i]][0]++;//统计每一组内有多少物品
g[p[i]][g[p[i]][0]] = i;//表示第p[i]组第g[p[i]][0]个的编号
}
for (int i = 1; i <= T; i++)//遍历每一组
for (int j = V; j >= 0; j--)//背包容量逆序遍历
for (int k = 0; k <= g[i][0]; k++)//遍历i组中的元素。由于j在此循环中不变,且j对于V是逆序遍历,所以f[j-w[x]]的数值虽然会随x的变化而变化,但它属于上一个j的状态是始终不变的,所以遍历完k后f[V]就会保留i组中只选一个的最优解,从而在局部上构成了01背包的模型
{
int x = g[i][k];
if (j >= w[x]) f[j] = max(f[j],f[j-w[x]]+c[x]);
}
cout << f[V] << endl;
return 0;
}
完[1]