背包问题:
一. 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;
}
4. 最值问题:
在讲完全背包之前首先可以了解一下完全背包的 01 背包的区别:
1. 完全背包问题:在完全背包问题中,每种物品可以选择无限次放入背包中。也就是说,每种物品的数量是无限的,可以选择放入背包中多次。这种情况下,动态规划转移方程通常是dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]] + v[i])。
2. 01背包问题:在01背包问题中,每种物品只能选择放入一次或不放入背包。也就是说,每种物品的数量是有限的,只能选择放入一次或不放入。这种情况下,动态规划转移方程通常是dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])。
总的来说,完全背包问题和01背包问题的主要区别在于物品的放入次数限制。
二. 完全背包:
1. 完全背包经典问题:
【问题描述】
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
【输入格式】
第一行:两个整数,M(背包容量,M<=100000)和N(物品数量,N<=10000);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
【输出格式】
仅一行,一个数,表示最大总价值。
【样例输入】
10 4
2 3
3 3
4 5
7 9
【样例输出】
15
解法一:用二维数组dp[i][j]表示,
dp[i][j]定义:前 i 件物品总重量不超过 j 的最优价值
则状态转移方程:dp[i][j]=max( dp[i-1][j], dp[i][j - w[i]] + c[i] )
注意:上述的状态转移方程的橙色部分为什么是 i 而不是和 01 背包一样是 i-1 呢?
理由如下:因为在完全背包问题中,每种物品可以选择无限次放入背包中,因此在计算当前状态dp[i][j]时,可以选择继续使用第i种物品(即dp[i][j - w[i]] + c[i]),而不需要考虑前一种物品的选择情况(即dp[i-1][j-w[i]] + c[i])
完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 10005
#define maxm 100005
int dp[maxn][maxm], w[maxn], c[maxn];
int main(){
int n, m;
// 数据输入
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++){
if(j<w[i]) dp[i][j] = dp[i-1][j];
else dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]]+c[i]);
}
}
cout << dp[n][m] << endl;
return 0;
}
解法 二:一维数组 dp[i]
dp[i]定义:重量不超过 i 的最大价值
则状态方程为:dp[i]=max(dp[i], dp[i-w[i]]+c[i])
完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 10005
#define maxm 100005
int dp[maxm], w[maxn], c[maxn];
int main(){
int n, m;
// 数据输入
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=w[i]; j<=m; j++){
dp[j] = max(dp[j], dp[j-w[i]]+c[i]);
}
}
cout << dp[m] << endl;
return 0;
}
2. 方案数问题:
1.【问题描述】
自然数拆分:
即给定一个自然数N,要求把N拆分成若干个正整数相加的形式,参与加法运算的数可以重复。求拆分的方案数 mod 100000的结果。1≤N≤4000。
【输入格式 】
一个整数n。
【输出格式】
输出一个数,即所有方案数
因为这个数可能非常大,所以你只要输出这个数 mod 100000 的余数即可。
【样例输入】
7
【样例输出】
14
【样例解释】
输入7,则7拆分的结果是
7=1+6
7=1+1+5
7=1+1+1+4
7=1+1+1+1+3
7=1+1+1+1+1+2
7=1+1+1+1+1+1+1
7=1+1+1+2+2
7=1+1+2+3
7=1+2+4
7=1+2+2+2
7=1+3+3
7=2+5
7=2+2+3
7=3+4
算法分析:这是一个典型的完全背包问题,1~N个数字就是 N 个物品,可以无限次使用,而数字 n 就是背包大小。此题要求的是方案数,则只需要将完全背包的状态转移方程中的 max改为求和即可。
完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
#define mod 100000
#define maxn 4005
int dp[maxn]; // dp[i]表示:和为 i 有多少种方案数
int main(){
int n;
cin >> n;
dp[0]=1; // 因为一个数也不选就是和为 0 也是一种方案,只是我们计算的结果是从 1~N而已
for(int i=1; i<= n;i++){
for(int j=i; j<=n; j++){
dp[j] += dp[j-i];
dp[j] %= mod;
}
}
// 因为计算的是 0~N 的 方案数,从而推出 数字 N 的方案数,
// 而我们要的是从 1~N 中能组成数字 N 的方案数,所以最终结果dp[n],要减去dp[0]
cout << dp[n]-1 << endl;
return 0;
}
2.【问题描述】
给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。样例:设n=3,m=10,要求输入和输出的格式如下:
【样例输入】
3 10 //3种面值组成面值为10的方案
1 //面值1
2 //面值2
5 //面值5
【样例输出】
10 //有10种方案
【数据规模】
1<=n<=1000
1<=m<=10000
完整代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
#define int long long
#define maxn 2000
#define maxm 10005
int dp[maxm], a[maxn];
signed main(){
int n, m;
cin >> n >> m;
for(int i=1; i<= n;i++)
cin >> a[i];
dp[0] = 1;
for(int i=1; i<=n ;i++){
for(int j=1; j<= m; j++)
dp[j] += dp[j-a[i]];
}
cout << dp[m] << endl;
return 0;
}
3. 有无统计问题:
4. 最值问题:
【问题描述】
1.最多个数:
输入n个正整数表示货币的面值,每个正整数ai有无限个,用这些货币来拼凑面值m。在所有不同的拼法中,用到的货币数最多的是多少?若不能拼凑,则输出-1.
【输入格式】
第一行,两个整数n和m,用空格隔开
接下来的1行有n个正整数,用空格隔开,表示面值
【输出格式】
一个整数,表示求得的最多的货币个数
【样例输入】
3 10
2 4 6
【样例输出】
5
【样例输入】
3 11
2 4 6
【样例输出】
-1
【说明】
【数据规模】
1< n<=100, 10<=m <= 100000
背包思想:完全背包,记录最大方案数
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
int main(){
int n, m;
// dp[i] 表示:和为 i 的最大方案数,即组成和为数字 i,所用的数字个数最多的个数是多少个
int dp[100005];
int a[105] = {0};
cin >> n >> m;
// dp 数组初始化:
// 求最大方案数,因此把 dp数组初始化为一个较小的数
// 如果求最小方案数则类似,将 INT_MIN改成 INT_MAX即可
for(int i=0; i<100005; i++)
dp[i] = INT_MIN; // INT_MIN是 int 类型的最小数
// 每张货币的面值是大于 0 的所以,组成 面值 0 的方案数因该为 0
dp[0] = 0;
for(int i=1; i<=n; i++)
cin >> a[i];
for(int i=1; i<=n; i++){
for(int j=a[i]; j<=m; j++){
// 记录和为 j 的最大方案数
dp[j] = max(dp[j],dp[j-a[i]]+1);
}
}
// 如果组成 面值为 m 的方案数存在,则
// 根据我们 dp 数组的定义,该值一定是最大值,即最大方案数,
// 否则就不存在输出-1
if(dp[m] > 0) cout << dp[m]<< endl;
else cout << -1 << endl;
return 0;
}
【问题描述】
2.货币组合:
有n种货币,每种货币有若干张,从中选择m张货币,可以组成一种面值,例如有2种面值(1和3的货币)选取5张,可以组成的面值如下表:
其中14的面值是无法组成的。
现输入货币种类n和选取的张数m,以及n种货币的币值,求不能表示的币值有多少种
【输入格式】
第一行两个正整数n和m;
第二行n个正整数,用空格隔开,表示不同的币值ai。
【输出格式】
一个正整数,表示不能表示的面值的个数
【样例输入】
2 5
1 3
【样例输出】
1
【数据规模】
1<=n<=100 1<=m<=10 1<=ai<=100
背包思想:完全背包,记录最小方案数
为什么记录最小方案数呢?
从 dp[i]的定义出发:即和为 i 的最小方案数。
且题目问的是不能表示的面值个数是多少,且一共可以用 m 张钞票来组合,那么如何才能表示出面值不能表示的情况呢?
可以这样思考🤔🤔:如果一个面值(即一个数)能表示的最小方案数都大于了我们所能使用的钞票数 m 那是不是就说明这个面值是我们不能表示出来的呢?
也因此我们在给 dp 数组初始化的时候就尽量给其初始化一个较大的数,这里代码初始化为 INT_MAX(即 int 类型的最大值)
完整代码如下:
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
int n, m;
cin >> n >> m;
int dp[10005], a[105]={0};
// dp 数组初始化
for(int i=0;i<10005 ;i++){
dp[i] = INT_MAX;
}
dp[0] = 0; // 每张钞票都是大于 0 的因此面值为 0 的方案数为应该初始化为 0
for(int i=1; i<=n; i++)
cin >> a[i];
// 将现有的钞票面值从小到大排序一下方便处理
sort(a+1, a+n+1);
// 一共 m 张钞票,经过上面的sort排序后最大的现有面值为a[n]
// 则所能表示的最大面值数为:m * a[n]
int Max = m * a[n];
for(int i=1; i<=n; i++){
for(int j=a[i]; j<=Max; j++){
// 记录最小方案数
dp[j] = min(dp[j], dp[j-a[i]]+1);
// cout << "dp[" << j << "]:" << dp[j] << endl; 打表测试而已
}
}
int ans = 0;
// 因为每张钞票都是大于 0 的,所以面值从 1 开始取到最大面值结束
for(int i=1; i<=Max; i++){
// 如果面值 i 的最小方案数大于我们所能用的钞票张数,就说明不能表示该面值
if(dp[i] > m){
ans++;
}
}
cout << ans << endl;
return 0;
}