目录
01背包问题
完全背包问题
多重背包问题
多重背包问题二
混合背包问题
二维费用背包问题
分组背包问题
背包问题求方案数
背包问题求具体方案
砝码称重问题
数字组合
动态规划算法(核心是状态的表示和状态的转移)
dp复杂度=状态量*转移计算量
动态规划算法介绍
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分成小问题进行解决,从而一步步获取最优解的处理算法。
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治算法不同的是,
适用于动态规划求解的问题,经分解得到的子问题往往不是互相独立的
。(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。 - 动态规划可以通过
填表的方式
来逐步推进,得到最优解。
01背包问题
01背包是在M件物品
取出若干件
放在空间为W的背包里
,每件物品的体积为W1,W2至Wn,与之相对应的价值为P1,P2至Pn。01背包的约束条件是给定几种物品,每种物品有且只有一个,并且有权值和体积两个属性。在01背包问题中,因为每种物品只有一个,对于每个物品只需要考虑选与不选两种情况。如果不选择将其放入背包中,则不需要处理。如果选择将其放入背包中,由于不清楚之前放入的物品占据了多大的空间,需要枚举
将这个物品放入背包后可能占据背包空间的所有情况。
思路一:图标填充
填表过程:
从物品一开始,先行再列
经过填表后可得出如下:
- v[i][0]=v[0][j]=0 //表示填入表第一行和第一列是0
- 当w[i]>j时:v[i][j]=v[i-1][j] //当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
- 当j>=w[i]时:v[i][j]=max{v[i-1][j],v[i]+v[i-1][j-w[i]]}
当准备加入的新增的商品的容量小于当前背包的容量,
装入方式:
v[i-1][j]表示上一个单元格的装入的最大值
v[i-1][j-w[i]]表示装入i-1个商品(不包括当前商品),剩余空间j-w[i]的最大值
注意: 具体的动态规划算法多种多样,但它们具有相同的填表格式。
对于01背包为什么是f[i-1][j-w[i]],因为如果是f[i]的话有可能已经将第i个物品装过了,然后又装了第i个物品,但是我们希望求的是在不装第i个物品的时候的最大值。
思路二:分析图
问题描述:
有 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
针对此题我们画出分析图:
注:
- f(i,j)表示,在满足从前i个物品中选,体积不超过j的集合(各种选法的集合),f(i,j)的值表示集合中的价值最大值(属性为max)
- f(i,j)表示集合中的某种属性(值),集合中有很多选法,但是到最后要落实到值上面
相关代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[N];
int main(){
int m,n;
cin>>m>>n;
for(int i=1;i<=m;i++){
cin>>v[i]>>w[i];
}
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++){
f[i][j]=f[i-1][j];
if(v[i]<=j) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
cout<<f[m][n];//答案即为最后一个物品尝试过,并且为背包的容积
return 0;
}
优化(滚动数组存储最大值,因为后一层的求解依赖于前一层)
相关代码:
#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 = m; j >= v[i]; j -- )
//为什么无需f[i][j]=f[i-1][j]?
//因为当优化成一维之后f[j]的值就是上一种物品f[j]的最大价值
//为什么是逆序?
//f[j]表示背包容量为j时的最大价值
//如果正序,假设f[3]=5,更新后f[3]=6,那么如果f[4]需要用到f[3]的值
//那么f[3]的值不是f[i-1][3],而是f[i][3],被“污染了”
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
总结:
动态规划中,思想就是大问题
化为小问题
,小问题再分解,得到一个统一的状态转移方程,但是规模最小的一个问题用的也是同一个状态转移方程
,我们把1号问题(最小)初始化了,或者就是从无到有,在无的时候初始化了,然后1号问题借助无的初始化融入到状态转移方程中,一般是后者,无也要初始化
动态规划往往以最后一步来分类
的,把一个大问题分解成很多小问题,这些小问题又相互联系,这个小问题的答案往往需要之前的问题的答案进行计算,小问题的规模不断扩大,并且随着小问题的不断解决,大问题也就有了答案
动态规划基本上都是以最后一步来划分集合,及最后我们实际选的一步(最基本的一步)来划分集合,比如01背包问题,我们最后一步选的是是否带上第i个物品,如果不带就是选f[i-1][j],如果带i就是f[i-1][j-v[i]]+w[i]
动态规划的每一个状态都是很多个方案综合起来的最优解,是暴搜的优化。
对于先选哪几类物品和容量是从大到小,还是从小到大对结果是不影响的
完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
问题分析
代码一
在01背包的基础上再加一个for循环来判断每种物品放多个的情况,时间复杂度较高
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];//f[i][j]:选i个物品的前提下,容量不超过j的最大价值
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){//第i个物品开始选择
for(int j=1;j<=m;j++){//容量
for(int k=0;k*v[i]<=j;k++){
//k从0开始,就包含了f[i][j]=f[i-1][j]的情况
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout<<f[n][m];
return 0;
}
代码二(优化一)
我们可以发现如下图规律
递推公式:满足所有情况,在这个公式的前提下就无需第三层for循环了,因为已经涵盖了for循环的作用。
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];//f[i][j]:选i个物品的前提下,容量不超过j的最大价值
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){//第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][j-v[i]]+w[i]);
}
}
cout<<f[n][m];
return 0;
}
优化后的代码和01背包很像,但是要注意区别
代码二(优化二)
优化成一维,但是和01背包的顺序不一样,01背包是从大到小,完全背包是从小到大
#include<iostream>
using namespace std;
const int N=1010;
int f[N];//f[j]:容量为j的最大价值数
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++){//第i个物品开始选择
for(int j=v[i];j<=m;j++){//因为f[j-v[i]]需要比f[j]先算出来
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
return 0;
}
多重背包问题
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例
10
分析
注:f[i][j]表示的是只从前i个物品中选,并且总体积不超过j的最大价值,但是f[i][j]需要不断迭代(根据集合划分的选法,集合划分是根据最终选择的策略进行划分),才能选定最大值。
代码一(枚举)
#include<iostream>
using namespace std;
const int N = 110;
int f[N][N];
int v[N],w[N],s[N];
int main()
{
int n,m;
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]);//k=0时已经包含了f[i-1][j]的这种情况
}
}
}
cout<<f[n][m];
return 0;
}
代码二(二进制优化)
二进制优化
将多重背包问题转化为01背包问题
任意一个数都可以用2的n次方表示,比如7可以由1+2+4
比如16直接可以由2的4次方表示
200可以分解为1、2、4、8、16、32、64、73
通过上述1、2…73可以凑出0~200之间的任意一个数
分解条件
三点:
(1)我们知道转化成01背包的基本思路就是:判断每件物品我是取了价值大还是不取价值大。
(2)我们知道任意一个实数可以由二进制数来表示,也就是2^0 ~ 2^k其中一项或几项的和。
(3)这里多重背包问的就是每件物品取多少件可以获得最大价值。
例如:物品A一共有63件,我们可以将它分成1、2、4、8、16、32件等六堆,我们可以将这六堆中的每堆都看成一件新的物品,转化成01背包问题就是这六堆物品,每堆我们取还是不取。然后这六堆物品的组合可以组成0~63的任意一个数。
或许会有疑问:怎么体现组合的过程呢?
两层for循环外加选择策略即可,手动模拟一下即可
#include<iostream>
using namespace std;
const int N = 12010,M=2010;
int v[N],w[N],s[N];
int f[M];
int main()
{
int n,m;
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;
//将多重背包问题转化为01背包问题
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];
return 0;
}
多重背包问题(二)
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
分析
#include<iostream>
using namespace std;
const int N = 110;
int f[N],s[N];
int v[N][N],w[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;j<=s[i];j++){
cin>>v[i][j]>>w[i][j];//第i组第j种物品的信息
}
}
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
//在f[i][j]的前提下,对第i组的每一种(k)物品进行遍历
//从而使f[i][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];
return 0;
}
混合背包问题
输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例
8
思路
根据题意描述可知,混合背包是将01背包、完全背包、多重背包结合起来。我们可以将多重背包问题转化为01背包问题,这样就只剩下01背包和完全背包,我们根据其性质分别进行处理即可。
代码
#include<iostream>
#include<vector>
using namespace std;
const int N = 1010;
int f[N];
struct thing{
int s;//种类
int v,w;//体积和价值
};
int main()
{
int n,m;
cin>>n>>m;
vector<thing> things;//存储物品
for(int i=1;i<=n;i++){
int v,w,s;
cin>>v>>w>>s;
if(s==-1){
things.push_back({-1,v,w});
}else if(s==0){
things.push_back({0,v,w});
}else{//将多重背包问题转化为01背包问题
int k=1;
while(s>=k){
things.push_back({-1,v*k,w*k});
s-=k;
k*=2;
}
if(s>0){
things.push_back({-1,v*s,w*s});
}
}
}
//进行计算
for(auto thing:things){
if(thing.s==-1){//01背包问题
for(int j=m;j>=thing.v;j--) f[j]=max(f[j],f[j-thing.v]+thing.w);
}else{//完全背包问题
for(int j=thing.v;j<=m;j++) f[j]=max(f[j],f[j-thing.v]+thing.w);
}
}
cout<<f[m];
return 0;
}
说明
对于背包问题,从哪个物品开始挑选、体积是从小到大还是从大到小对结果均无影响。对于01背包和完全背包混合计算,并不是说必须全是01背包才能计算,或者必须全是完全背包才能计算,对于两者的迭代公式,均是对值的判断,且每一种背包的每一个物品迭代后留下的均是每一种体积的最大价值,无论下一轮是哪一种背包问题,迭代时仅需要判断值即可,上一轮是哪种背包问题并不影响。
二维费用的背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
输入格式
第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000
输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8
分析
因为多加了一个重量,在01背包的基础上多加一层循环即可
代码
#include<iostream>
using namespace std;
const int N=110;
int f[N][N];
int main()
{
int n;
cin>>n;
int v,m;//体积、重量
cin>>v>>m;
for(int i=1;i<=n;i++){
int a,b,c;//体积、重量、价值
cin>>a>>b>>c;
for(int j=v;j>=a;j--){//体积
for(int k=m;k>=b;k--){//重量
//此代码是优化后的代码,将三维优化成二维
//若是三维则需要f[i][j][k]=f[i-1][j][k]
//为什么优化后就不需要?因为f[j][k]保存的就是上一次的最大价值
f[j][k]=max(f[j][k],f[j-a][k-b]+c);
}
}
}
cout<<f[v][m];
return 0;
}
分组背包问题
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
分析
因为有了分组的概念,在01背包的基础上加一个for循环来判断组数即可
代码
#include<iostream>
using namespace std;
const int N = 110;
int f[N],s[N];
int v[N][N],w[N][N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
for(int j=1;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--){//01背包优化成一维需要从大到小进行判断
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];
return 0;
}
背包问题求方案数
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示 方案数 模 109+7 的结果。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
2
分析
因为在求f[i][j]时,j表示的含义是在<=j的情况下的最大值,可以填满,也可以不填满,且f[i][j]方程在转移时是根据值来转移的,并不能知道j是否被填满,所以这里我们理解g[i][j]的j的含义时就理解成当背包容积为j时的方案数即可
注意:求方案数容量j的下标一般从0开始
代码一(二维)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n, m;
int w[N], v[N];
int f[N][N], g[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i];
//01背包求最值
//f[i][j]表示在前i个物品中,体积不超过j的情况下的最大价值
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]);
}
}
//g[i][j]表示在前i个物品中选,背包体积为j的最优方案数
for(int i=0;i<=n;i++) g[i][0]=1; //无论是哪种物品参与,当背包容积为0时只有一种方案
for(int i=0;i<=m;i++) g[0][i]=1;//从0个物品中选,背包容积无论变为多少都只有一种方案
for (int i = 1; i <= n; ++ i)
{
for (int j = 0; j <= m; ++ j)//当求方案数时,j一般从0开始,求最值时0、1均可以
{
int left = f[i-1][j],right;
if(j>=v[i]){
right= f[i-1][j - v[i]] + w[i];
}else{
right=-1;
}
if (left > right) g[i][j] = g[i-1][j];
else if (left < right) g[i][j] = g[i-1][j - v[i]];
else g[i][j] = g[i-1][j] + g[i-1][j - v[i]];
g[i][j] %= mod;
}
}
//因为最优解一直在转移,最后会转移至g[n][m],及时m没有填满
cout << g[n][m]<< endl;
return 0;
}
代码二(一维)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1000000007;
int n, m;
int f[N], g[N];
int v[N], w[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 0; i <= m; i ++ ) g[i] = 1;
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= v[i]; j -- )
{
int left = f[j], right = f[j - v[i]] + w[i];
f[j] = max(left, right);
//优化成一维之后,g[j]的计算必须在求f[i]时计算,因为f[i]只保存了当前选i物品的最大值
if (left > right) g[j] = g[j];
else if (left < right) g[j] = g[j - v[i]];
else g[j] = g[j] + g[j - v[i]];
g[j] %= mod;
}
cout << g[m] << endl;
return 0;
}
代码三(思路二)
注释中解释常见疑问
#include<iostream>
using namespace std;
const int N = 1010,mod = 1e9+7;
int f[N][N];
int v[N],w[N];
int g[N][N];
int main()
{
int n,m;
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]);
}
}
g[0][0] = 1;
for(int i = 1;i <= n;i ++){
for(int j = 0;j <= m;j ++){
//为什么需要判断条件,而不能直接转移?
//因为如果f[i][j] != f[i-1][j],则表示f[i][j]这种情况并不是由i-1转移过来的
//不能判断是否选了i
//关于g[i][j]=(g[i][j]+g[i-1][j])%mod;
//因为g[i][j]的默认值为0,也可写成g[i][j]=g[i-1][j]的形式
if(f[i][j] == f[i-1][j]) g[i][j]=(g[i][j]+g[i-1][j])%mod;
if(j>=v[i] && f[i][j] == f[i-1][j-v[i]]+w[i]) g[i][j] = (g[i][j] + g[i-1][j-v[i]])%mod;
}
}
int res = 0;
for (int j = 0; j <= m; ++ j)
{
if (f[n][j] == f[n][m])
{
res = (res + g[n][j]) % mod;
}
}
cout << res << endl;
return 0;
}
代码四(思路二优化)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n, m;
int w[N], v[N];
int f[N], g[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i];
g[0] = 1;
for (int i = 1; i <= n; ++ i)
{
for (int j = m; j >= v[i]; -- j)
{
int temp = max(f[j], f[j - v[i]] + w[i]), c = 0;//借助临时变量来存储
if (temp == f[j]) c = (c + g[j]) % mod;
if (temp == f[j - v[i]] + w[i]) c = (c + g[j - v[i]]) % mod;
f[j] = temp, g[j] = c;//为什么可以赋值?因为物品i在不断更新,最优解可能会随着i变化
}
}
int res = 0;
for (int j = 0; j <= m; ++ j)
{
if (f[j] == f[m])
{
res = (res + g[j]) % mod;
}
}
cout << res << endl;
return 0;
}
背包问题求具体方案
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
1 4
分析
要求具体方案且要求字典序最小,那么我们在求f[i][j]时需要将物品序号大的先进行选择,这样从序号大的到序号小的求出的f[i][j],我们可以根据f[i][j]的值从序号小的物品进行判断。比如f[i][j]==f[i+1][j-v[i]]+w[i],则可以判断当i+1->i时,选择了i。依次类推即可,注意要判断容量和物品体积的关系。
代码
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
//因为方案是从小到大输出,所以我们在求最值时从大到小遍历
//从大到小遍历后,那么序号小的物品就有最值的最终值,因为最后才选择
//因为要输出方案,所以就不用一维数组进行优化了
for(int i=n;i>=1;i--){
for(int j=0;j<=m;j++){//关于j从0开始还是1开始,一般统计方案时需要从0开始
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]);
}
}
int val = m;
for(int i=1;i<=n;i++){
//只要可以选i就选,f[i][val]==f[i+1][val-v[i]]+w[i]综合所有情况的得出的代码
if(f[i][val]==f[i+1][val-v[i]]+w[i] && val>=v[i]){//说明从i+1到i选了i,如果没有选就跳过
//如果val<v[i]说明就不选,要注意这个特判
cout<<i<<' ';
//回到体积为val-v[i]的最大值,此时是f[i+1][val-v[i]],该判断当前最大值是否由选择i+2转移过来
val-=v[i];
}
}
return 0;
}
砝码称重(集合值属性为bool类型)
输入样例:
3
1 4 6
输出样例:
10
样例解释
能称出的 10 种重量是:1、2、3、4、5、6、7、9、10、11。
1 = 1;
2 = 6 − 4 (天平一边放 6,另一边放 4);
3 = 4 − 1;
4 = 4;
5 = 6 − 1;
6 = 6;
7 = 1 + 6;
9 = 4 + 6 − 1;
10 = 4 + 6;
11 = 1 + 4 + 6。
分析
闫式DP分析法
tip:|=运算
代码一(偏移量的使用)
#include<iostream>
using namespace std;
const int N=110,M=200010,B=M/2;//B是偏移量,防止下标为负数
bool f[N][M];
int w[N];
int main()
{
int n;
cin>>n;
int m=0;
for(int i=1;i<=n;i++){
cin>>w[i];
m+=w[i];
}
//假设砝码放左边为正,放右边为负,物品放左边称出的重量为负,物品放右边平衡则称出的重量为正
f[0][B]=true;//因为有负数,所以我们加个偏移量
for(int i=1;i<=n;i++){
for(int j=-m;j<=m;j++){
//易知重量j可能由很多种组合的砝码称出来
f[i][j+B] |= f[i-1][j+B];//当不选砝码i称出重量j
//为什么要有if限制条件?
//因为是枚举每种情况,有些情况不符合条件,当前重量加上某个砝码不可能>=m
if(j+w[i]<=m) f[i][j+B] |= f[i-1][j+w[i]+B];//当砝码i放在左边时
//当砝码i放在右边时
if(j-w[i]>=-m) f[i][j+B] |= f[i-1][j-w[i]+B];
}
}
int ans=0;
for(int i=1;i<=m;i++){
if(f[n][i+B]) ans++;
}
cout<<ans;
return 0;
}
代码二(对称性质的运用)
#include<iostream>
using namespace std;
const int N=110,M=100010;
bool f[N][M];
int w[N];
int main()
{
int m=0;
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>w[i];
m+=w[i];
}
//-m~m之间是对称的
f[0][0]=true;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
//虽然我们只枚举1-m的情况,但是每种砝码都有放左边或者放右边的情况
//只不过认为物品在左右两边平衡时称出来的重量为正数
f[i][j]= f[i-1][j] || f[i-1][abs(j-w[i])];
if(j+w[i]<=m) f[i][j] = f[i][j] || f[i-1][j+w[i]];
}
}
int ans=0;
for(int i=1;i<=m;i++){
if(f[n][i]) ans++;
}
cout<<ans;
return 0;
}
数字组合(属性为数量)
给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。
输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数,表示 A1,A2,…,AN。
输出格式
包含一个整数,表示可选方案数。
数据范围
1≤N≤100,
1≤M≤10000,
1≤Ai≤1000,
答案保证在 int 范围内。
输入样例:
4 4
1 1 2 2
输出样例:
3
分析
代码一
#include<iostream>
using namespace std;
const int N=110,M=1e5+10;
int f[N][M];
int w[N];
int main()
{
int n , m;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++){
cin >> w[i];
}
f[0][0] = 1;//当在前0个数组中选,值为0时的方案只有一种
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= m;j ++){
//过程描述:
//假设f[3][4]=5,即在前3中物品中选择,和为4的选法有5中
//那么在f[4][4]时,此时值为0,需要先赋值f[4][4]=f[3][4],然后再加上f[4][j-w[4]]的情况
//这样f[i][j]先继承f[i-1][j],然后再加上选i的情况,这样遍历到最后一个物品时就是最终的值
f[i][j] = f[i-1][j];//不选i的方案数
if(j >= w[i]) f[i][j] += f[i-1][j-w[i]]; //选i的方案数需要与不选i的方案数相加
}
}
cout<<f[n][m];
return 0;
}
代码二(优化)
#include<iostream>
using namespace std;
const int N=110,M=1e5+10;
int f[M];
int w[N];
int main()
{
int n , m;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++){
cin >> w[i];
}
f[0] = 1;//和为0的情况只有一种方案就是啥都不选
for(int i = 1; i <= n; i ++){
for(int j = m; j >= w[i];j --){
//为什么无需f[i][j]=f[i-1][j]
//因为优化为一维数组后,当选到i时f[0-m]已经是i-1的方案数,自动继承了
if(j >= w[i]) f[j] += f[j-w[i]]; //选i的方案数需要与不选i的方案数相加
}
}
cout<<f[m];
return 0;
}