01背包问题:
1. 经典01 背包问题:
题 1:
【问题描述】
一个旅行者有一个最多能用m公斤的背包,现在有n件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn.若每种物品只有一件求旅行者能获得最大总价值。
【输入格式】
第一行:两个整数,M(背包容量,M<=200)和N(物品数量,N<=30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】
10 4
2 1
3 3
4 5
7 9
【样例输出】
12
解法一:
用二维数组 dp[n][m]表示,
dp[i][j]定义:
dp[i][j]表示在这个包包里可放前0~i 个物品(即放前 i 个物品),总重量不超过j 的最大价值。
那么dp[i][j]可以由以下两种方式得到:
1.dp[i][v] = dp[i-1][v],即 dp[i][j]等于包包里可放前 i-1 个物品,且背包重量不超过 v 的最大价值,这是没放第 i 件物品的情况。
2.dp[i][v] = dp[i-1][v-w[i]]+c[i],通俗点讲,这里表示放前要放第 i 件物品的情况,为什么是这个式子呢?如何描述更清晰呢?
其实可这样思考:
首先 dp[i][j]本身定义是:dp[i][j]表示在这个包包里可放前0~i 个物品(即放前 i 个物品),总重量不超过j 的最大价值。那么求 dp[i][j]时肯定已经由递推方式推出了放前 0~i-1 个物品是的最大价值,因此此时我们要再放第 i 个物品,就必须保证背包的容量可以放得下第 i 个物品才行嘛,不然包包都撑坏了(这年头包包还是蛮贵的哈哈哈),所以有背包的容量应该为
v-w[i],即v-w[i]存在(大于等于 0)才可以放得下第 i 个物品,也可以理解v-w[i]为要放第 i 个物品,就得给背包腾出第 i 个物品所需的重量 w[i]来,此时的v-w[i]就表示腾出第 i 个物品的空间后还剩多少可用空间。
到这我们就该在 dp[i-1][v-w[i]]后面加上第 i 个物品的价值了,这就可以理解为,你再前面给第 i 个物品腾出了那么大的空间,就是为了装第 i 个物品嘛,那咱装了第 i 个物品不得给它的价值也加上嘛,所以有dp[i][v] = dp[i-1][v-w[i]]+c[i]
到这我们就知道了 dp[i][j]可以由上述两种方式推出,那么取哪个一个作为状态转移方程的推导式子呢?
根据题目我们要求的是最大价值,那么直接取上面两种方式得出的价值的最大的一个就可以了,因此状态转移方程:
dp[i][v]=max( dp[i-1][v], dp[i-1][v-w[i]]+c[i] )
完整代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 35
#define maxm 205
int dp[maxn][maxm];
int w[maxn], c[maxn];
int main(){
int m, n;
// 数据输入
cin >> m >> n;
int i, j;
for(i=1; i<=n; i++)
cin >> w[i] >> c[i];
// 数据处理
for(i=1; i<=n; i++){
for(j=m; j>0; j--){
if(j-w[i] >= 0)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]]+c[i]); // 递推公式
else dp[i][j] = dp[i-1][j];
}
}
cout << dp[n][m];
cout << endl;
return 0;
}
使用二维数组存储各子问题时方便,但当maxm较大时,如maxm=2000时不能定义二维数组f,怎么办,其实可以用一维数组。
本题解法二:
设dp[v]表示重量不超过 v 的最大价值,
则状态转移方程为:dp[v] = max( dp[v], dp[v-w[i]]+c[i] )
完整代码如:
#include<iostream>
#include<algorithm>
using namespace std;
#define maxn 10000
#define maxm 10000
int dp[maxm];
int w[maxn], c[maxn];
int main(){
int m, n;
// 数据输入
cin >> m >> n;
int i, j;
for(i=1; i<=n; i++)
cin >> w[i] >> c[i];
// 数据处理
for(i=1; i<=n; i++){
for(j=m; j>=w[i]; j--){
dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
}
}
cout << dp[m];
cout << endl;
return 0;
}
题 2:
装箱问题
有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每个物品有一个体积(正整数)。
要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
【样例输入:】
一个整数,表示箱子容量
一个整数,表示有n个物品
接下来n行,分别表示这n 个物品的各自体积
例:
24
6
8
3
12
7
9
7
【样例输出】
一个整数,表示箱子剩余空间
例:
0
题型分析:典型的 01背包问题,n 个物品放入有限空间的背包里,求最大价值是多少?这里只是求最小剩余空间而已,用箱子总空间减去最大占用空间即 V-dp[v]即可。
完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
#define maxV 20005
#define maxn 40
int dp[maxV], a[maxn];
int main(){
int V, n;
cin >> V >> n;
int i, j;
for(i=1; i<=n; i++){
cin >> a[i];
}
for(i=1; i<=n; i++){
for(j=V;j>=a[i]; j--){
dp[j] = max(dp[j],dp[j-a[i]]+a[i]);
}
}
cout << V - dp[V];
cout << endl;
return 0;
}
2. 方案数问题:
题 1:
数字组合
【题目描述】
在 N个数中找出其和为M的若干个数。先读入正整数N(1<N<100)和M(1<M<10000), 再读入N个正数(可以有相同的数字,每个数字均在1000以内), 在这N个数中找出若干个数, 使它们的和是M, 把满足条件的数字组合都找出来以统计组合的个数,输出组合的个数(不考虑组合是否相同)。要求你的程序运行时间不超过1秒。
【输入格式】
第一行是两个数字,表示N和M。
第二行起是N个数。
【输出格式】
一个数字,表示和为M的组合的个数。
【样例输入】
4 4
1 1 2 2
【样例输出】
算法分析:这是一道经典的 01 背包问题,N 个数就代表 N 个物品,M 就代表背包的重量,在外层循环到 i 时代表前 i 个物品选中。
设dp[j]定义:和为 j 有多少种方案。即把前面的例题的 max 改为求和即可!
背包思想:将数字看着体积,方案数看做是价值
完整代码如下:
#include <iostream>
using namespace std;
#define maxm 10005
#define maxn 105
int dp[maxm], nums[maxn];
int main(){
int N, M;
cin >> N >> M;
for(int i=1; i<=N; i++)
cin >> nums[i];
dp[0] = 1; // 当和为 0 的时候只有一个都不选这一种方案,所以 dp[0]=1
for(int i=1; i<=N; i++){
for(int j=M; j>=nums[i]; j--){
//dp[j] += dp[j - nums[i]]; 也可以写成下面的样子,方便理解一点
dp[j] = dp[j] + dp[j - nums[i]];
}
}
cout << dp[M] << endl;
return 0;
}
3. 有无统计:
题 1:
硬币🪙:
给定N种硬币,其中第i种硬币的面值为Ai,共有Ci个。从中选出若干个硬币,把面值相加,若结果是S,则称“面值s能够拼成”。求1~M能够拼成的面值有多少个?
其实这也是一个多重背包问题
样例输入:
一行输入 n 和 m
接下来 n 行分别输入 Ai和 Ci
2 5
1 1
2 1
样例输出:
一个数字表示所能表示的面值种类数
3
算法分析:只需要统计每个状态下是否能够拼成面值为 s 即可,
设 dp[i]定义:硬币和为 i 有多少种方案
因此我们只需要知道 dp[i]的状态是否非 0 即可,
因此状态转移方程: dp[k] = dp[k] | dp[k - a[i]], 即只要判断是否有方案可以退出 dp[k]是否为 0即可,
并将将其真假值(0 或 1)赋值给 dp[k]
然后遍历每一种面值的真假状态,即遍历 dp数组,并用 ans 累加记录可以拼凑出的面值个数即可。
完整代码如下:
#include <iostream>
using namespace std;
int dp[100005], a[105], c[105];
int main(){
int n, m;
// 数据输入
cin >> n >> m;
for(int i=1; i<=n; i++)
cin >> a[i] >> c[i];
// 数据处理
dp[0]=1; // 当面值为0时,是可以拼成的,所以 dp[0]=1
for(int i=1; i<=n; i++){ // 表示前 i 种硬币被选中
for(int j=1; j<=c[i]; j++){ // 每种硬币被选了多少个
for(int k=m; k>=a[i]; k--){
//
// dp[k] = dp[k] | dp[k - a[i]] 这行代码同下,但相对于下面的代码好理解一点
dp[k] |= dp[k - a[i]];
}
}
}
int ans=0;
for(int i=1; i<=m; i++)
ans += dp[i];
cout << ans << endl;
return 0;
}
题二:
【问题描述】
你有一架天平和N个砝码,这N个砝码重量依次是W1, W2,…,WN请你计算一共可以称出多少种不同的重量?注意砝码可以放在天平两边。
【输入格式】
输入的第一行包含一个整数N。
第二行包含N个整数:Wi, W2: …,WN
【输出格式】
输出一个整数代表答案。
【样例输入】
3
1 4 6
【样例输出】
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。
【评测用例规模与约定】
对于50%的评测用例,1<=N<=15。
对于所有评测用例,1<=N<=100, N个砝码总重不超过100000。
算法分析:使用两次 01 背包
第一次:同上一题硬币类似:dp[j] |= dp[j-w[i]
第二次:dp[j] = dp[j-w[i]], 因为天平两边都可以放砝码,则在已知的可以称出的重量基础上减去某个砝码的重量就可以得到一个新的重量
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
#define maxn 105
#define maxW 100010
int dp[maxW], w[maxn];
bool cmb(int a, int b){
return a>b;
}
int main(){
int n;
// 数据输入
cin >> n;
for(int i=1; i<=n; i++)
cin >> w[i];
sort(w+1, w+1+n, cmb);
// 数据处理
dp[0]=1; // 因为当选 0 个砝码时,是可以算上,可以构成 0 这个重量的
for(int i=1; i<=n; i++){
for(int j=100000; j>=w[i]; j--){
// dp[j] = dp[j] | dp[j-w[i]]; // 该式与下面的式子同理
dp[j] |= dp[j-w[i]]; // 计算前 i 个砝码构成重量为 j 的方法数,是否非 0,这里和上一题硬币题类似
}
// 由于天平两边都可以放砝码做减法得出新的重量,因此
for(int k=w[i]; k<=100000; k++){
if(dp[k]==1){ // 即 k 这个重量是可以组成的,就可以说明 k 减去某个砝码的重量就得到了新的重量
dp[k-w[i]] = 1;
}
}
}
int cnt=0;// 记录答案
for(int i=1; i<=100000; i++)
if(dp[i])
cnt++;
cout << cnt << endl;
return 0;
}