0/1背包 多重背包 完全背包 混合背包 分组背包 二维费用背包 背包拆分自然数C++

动态规划

理论上来说贪心的速度是很快的:

O(n)(正常)到O(nlogn)(优先队列)

但是由于贪心太难以证明,子问题最有不一定全局最优等问题,故可以用一种全新的方法:

动态规划

当然,虽然它的时间复杂度是O\left ( nm \right ),但是以牺牲了一部分空间为代价的O\left ( nm \right )的空间复杂度)。

动态规划的使用条件


重复子问题:

存在子问题重复计算。
具备最优子结构:

后面的状态可以靠前面子问题推导。
无后效性:

已解子问题,不会受到后续决策影响。(跟贪心一样)

0/1背包

0/1背包用于:选n个不可分割的东西,求能选出的最大值,每个东西只选一次

例题

题面

洛谷P1048icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1048

样例

输入
6 4
1 2
1 1
3 5
5 7
输出
9

解析

其实这道题可以用记忆化搜索,但鉴于为了抢最优时间,还是用0/1背包较好

模拟

主要是csdn显示不出来长表格,故只能用照片的形式

状态转移方程

d[i][j]=\max\begin{cases} d[i-1][j]\\ d[i-1][j-v[i]]+w[i] \end{cases}v[i]\leqslant j

代码:朴素版
空间复杂度

O\left ( nm \right )

时间复杂度

O\left ( nm \right )

代码

存储了数组的全部信息,就是上面的表格

#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码
using namespace std;
int dp[105][1005];//二维数组记录表格
int n,m;
inline long long read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1; 
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入
int main(){
	m=read();
	n=read();
	for(int i=1;i<=n;i++){//模拟选择的是第几号物品 
		int a,b;
		a=read();
		b=read();
		for(int j=0;j<=m;j++){//模拟背包容量 
			if(j<a)	dp[i][j]=dp[i-1][j];
			else dp[i][j]=max(dp[i-1][j-a]+b,dp[i-1][j]);
			//状态转移数组 
		}
	}
	cout<<dp[n][m];//输出表格中背包容量为m,在考虑所有的物品的情况之后的正确答案
	return 0;
}
​
代码:优化版
空间复杂度

O\left ( m \right )

时间复杂度

O\left ( nm \right )

代码

存储了数组的全部信息,就是上面的表格

其实可以存在一个一维数组内,把选择第几号物品那一步删去,降低空间复杂度。但是由于需要左侧的值,故要从大到小逆序更新,保留左侧的值。

​
#include<bits/stdc++.h>
using namespace std;
int n,m,dp[1005];//一维滚动数组记录表格
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1; 
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入
int main(){
	m=read();
	n=read();
	for(int i=1;i<=n;i++){//模拟选择的是第几号物品 
		int a,b;
		a=read();//存储代价
		b=read();//存出价值
		for(int j=m;j>=a;j--) //模拟背包容量
			dp[j]=max(dp[j],dp[j-a]+b);//状态转移数组 
	}
	printf("%d",dp[m]);//输出表格中背包容量为n,在考虑所有的物品的情况之后的正确答案
} 

​

多重背包

多重背包用于:选n个不可分割的东西,求能选出的最大值,每个东西可以选k次

例题

题面

洛谷P1776icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1776

样例

输入
4 20
3 9 3
5 9 1
9 4 2
8 1 3
输出
47

解析

每种物品有C[i]件,其实可以理解为有C[i]种该物品,每种物品只有一件,由此转换成0/1背包。

使用上一题的方法,把维度i给滚掉

状态转移方程

d[i]=\max\begin{cases} d[i]\\ d[i-v[i]]+w[i] \end{cases}

代码:朴素版
空间复杂度

\max\begin{cases} O(M)\\ O(\sum_{i=1}^{N}C{_{i}}) \end{cases}

时间复杂度

O(M*\sum_{i=1}^{N}C_{i})

可以发现,下面的代码可能会超时,故我们用二进制拆分

#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int n,m,x,y,z;
int cnt;//记录号数 
int f[N];//一维数组记录表格
int w[N];//记录代价 
int v[N];//记录价值 
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>x>>y>>z;
		for(int j=1;j<=z;j++){//把这个点变成z个这样的点 
			cnt++;
			v[cnt]=x;//记录价值 
			w[cnt]=y;//记录重量 
		}
	}
	for(int i=1;i<=cnt;i++){//模拟选择第几号点 
		for(int j=m;j>=w[i];j--){//模拟背包容量 
			f[j]=max(f[j],f[j-w[i]]+v[i]);
			//状态转移方程 
		}	
	}
	printf("%d",f[m]);
	return 0;
}
二进制拆分

二进制拆分是指将一个数拆成多个二的次方数相加的拆分

以15为例:

拆出来的数为:1,2,4,8,7

数字组法
11
2

2

31+2
4

4

51+4
6

2+4

71+2+4
8

8

91+8
10

2+8

11

1+2+8

12

4+8

131+4+8
14

2+4+8

15

7+8

可以看出来,当你把一个数进行二进制拆分后,一定能用拆出来的数字组成小于等于被拆数字的数字

代码:优化版
空间复杂度

\max\begin{cases} O(M)\\ O(\sum_{i=1}^{N}\log C{_{i}}) \end{cases}

时间复杂度

O(\sum_{i=1}^{N}\log C{_{i}})

#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码
using namespace std;
const int M=4e4+5;
const int N=1e5+5;
int v[N];//记录价值 
int w[N];//记录代价 
int dp[M];//一维数组记录表格
int t,m,n;
int cnt;//记录号数 
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入
int main(){
	n=read();
    m=read();
    for(int i=1;i<=n;i++){
		int x,y,z;
        x=read();
        y=read();
    	z=read();
        int a=1;
        while(z>=a){//把这个点变成z个这样的点 
            cnt++;
			w[cnt]=a*y;//记录价值 
            v[cnt]=a*x;//记录重量 
            z-=a;
            a*=2;//二进制拆分 
        }
        if(z){//处理剩下的值 
            cnt++;
			w[cnt]=z*y;
            v[cnt]=z*x;
        }
    }
    for(int i=1;i<=cnt;i++){//模拟选择第几号点
		for(int j=m;j>=w[i];j--){//模拟背包容量 
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
			//状态转移方程 
        }
    }
    printf("%d",dp[m]);
	return 0;
}

完全背包

完全背包用于:选n个不可分割的东西,求能选出的最大值,每个东西可以选无数次

例题

题面

洛谷P1616icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1616

样例

输入
70 3
71 100
69 1
1 2
输出
140

解析

我们可以去枚举到底能放多少个第i件物品,从而变成0/1背包问题

不过要注意的是如果还是要省略掉第1维的话,需要从小往大枚举

状态转移方程

d[j]=\max\begin{cases} d[j]\\ d[j-k*v[i]]+w[i]*k \end{cases}

代码
时间复杂度

O(nm)

空间复杂度

O(m)

​#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码
using namespace std;
const int N = 1e7 + 5;
int n, m, dp[N];
inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c>'9') {
		if (c == '-') f = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9') {
		x = x * 10 + c - '0';
		c = getchar();
	}
	return x * f;
}//快速读入
int main() {
	m = read();
	n = read();
	for (int i = 1; i <= n; i++) {//枚举物品
		int a, b;
		a = read();
		b = read();
		for (int j = a; j <= m; j++) {//枚举背包容量
			dp[j] = max(dp[j], dp[j - a] + b);//状态转移方程
		}
	}
	printf("%d", dp[m]);
}

混合背包

混合背包其实就是在不同的情况下,选择不同的背包进行dp

例题

题面

洛谷P1833icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1833

样例

输入
6:50 7:00 3
2 1 0
3 3 1
4 5 4
输出
11

解析

针对可以用无数次的物品采用完全背包;

针对可以用1次的物品和有限次数次的物品用多重背包;

故建立一个数组存储用哪一个背包

状态转移方程

因为要用两种背包,故有两种状态转移方程

完全背包

d[j]=\max\begin{cases} d[j]\\ d[j-k*v[i]]+w[i]*k \end{cases}

多重背包

d[i]=\max\begin{cases} d[i]\\ d[i-v[i]]+w[i] \end{cases}

代码
#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码
using namespace std;
int n;
const int N=1e5+5;
const int M=1e6+5;
int c[N];//记录价值
int w[N];//记录代价
int p[N];//记录可以选的次数
int f[M];//记录背包
int cnt;//记录共有多少个数
inline long long read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入
int main (){
	int h,mi,h1,mi1;
	char ch;
	cin>>h>>ch>>mi>>h1>>ch>>mi1;
    int m=h1*60+mi1-h*60-mi;//时间转换
    cin>>n;
	for(int i=1;i<=n;i++){
		int x,y,z;
		x=read();
        y=read();
        z=read();
        if(!z){
			cnt++;
			c[cnt]=x;
			w[cnt]=y;
			p[cnt]=z;
			continue;
		}//如果为0证明可选无数次,跳过下面的步骤,用另一种的方式存储
		int a=1;
		while(a<=z){
			cnt++;
			c[cnt]=x*a;
			w[cnt]=y*a;
			p[cnt]=1;
			z-=a;
			a*=2;
		}//二进制拆解
		if(z){
			cnt++;
			c[cnt]=x*z;
			w[cnt]=y*z;
			p[cnt]=1;
		}//处理剩余的数
	}
    for(int i=1;i<=cnt;i++){
    	if(p[i]==0){
    		for(int j=c[i];j<=m;j++)	f[j]=max(f[j],f[j-c[i]]+w[i]);
    		continue;
		}//如果为0就用完全背包
    	for(int j=m;j>=c[i];j--)	f[j]=max(f[j],f[j-c[i]]+w[i]);//否则用多重背包
	}
	cout<<f[m];
	return 0;
}

分组背包

可以用来处理这类问题:
有n各组,在每个组中选n一个数,给定背包大小和每个组中每个数的代价和价值,求能选出的最大数

例题

题面

ACWing 9. 分组背包问题icon-default.png?t=N7T8https://www.acwing.com/problem/content/description/9/

样例

输入
3 5
2
1 2
2 4
1
3 4
1
4 5
输出
8

解析

其实就是0/1背包的题,无非就是在加一层循环,枚举每一个组内的值

状态转移方程

d[j]= \max\begin{cases} d[j]\\ d[j-v[i][k]]+w[i][k] \end{cases}v[i][k]\leqslant j

代码
#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码 
using namespace std;
int n,m;
int dp[105];//模拟背包容量 
int v[105][105];//记录每个组中每个值的代价 
int w[105][105];//记录每个组中每个值的价值 
int c[105];//记录每个组有几个物品 
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1; 
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入 
int main(){
    int t;
    t=read();
	m=read();
    for(int k=1;k<=t;k++){//读入各组中的东西 
		c[k]=read();//读入组数 
        for(int i=1;i<=c[k];i++){
			int a,b;
			v[k][i]=read();//读入每个组中每个值的代价  
			w[k][i]=read();//读入每个组中每个值的价值 
        }
    }
    for(int i=1;i<=t;i++){//枚举组数 
		for(int j=m;j>=0;j--){//枚举背包大小 
            for(int k=1;k<=c[i];k++){//枚举各组中的物品 
				if(j>=v[i][k]){//防止数组越界 
					dp[j]=max(dp[j],dp[j-v[i][k]]+w[i][k]);//状态转移方程 
                } 
            }	
		}
    }
	printf("%d",dp[m]);
}

二维费用背包

可以用来处理这类问题:
有n个数,每个数有K_{i}的重量,V_{i}的体积,W_{i}的体积,要求在体积和重量都不超过要求的情况下,装下最多价值的物品

tips:状态不够,维数来凑

例题

题面

ACWing 8. 二维费用的背包问题icon-default.png?t=N7T8https://www.acwing.com/problem/content/description/8/

样例

输入
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出
8

解析

把dp数组多间一个维度即可,和0/1背包的写法差不多

状态转移方程

d[j][g] \max\begin{cases} d[j][g]\\ d[j-v[i]][g-k[i]]+w[i]\end{cases}v[i]\leqslant j,k[i]\leqslant g,

代码
时间复杂度

O(N*V*M)

空间复杂度

O(V*M)

#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//优化代码 
using namespace std;
int N,M,V;
int dp[1005][1005];//记录背包的容积和重量 
int v[1005];//记录值的体积 
int k[1005];//记录值的重量 
int w[1005];//记录值的价值 
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1; 
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//输入优化 
int main(){
    N=read();//输入有几个值 
	V=read();//输入容量上限 
	M=read();//输入重量上限 
    for(int i=1;i<=N;i++){//模拟第几个值 
    	v[i]=read();//输入值的体积 
        k[i]=read();//输入值的重量 
        w[i]=read();//记录值的价值 
		for(int j=V;j>=v[i];j--){//模拟背包容量 
			for(int g=M;g>=k[i];g--){//模拟背包重量 
				dp[j][g]=max(dp[j][g],dp[j-v[i]][g-k[i]]+w[i]);//状态转移方程 
            }
        }
    }
	printf("%d",dp[V][M]);
} 

背包拆分自然数

可以用来处理这类问题:
给定一个自然数 n,把 n 拆分成若干个自然数相加之和

例题

题面

洛谷P1164icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1164

样例

输入
4 4
1 1 2 2
输出
3

解析

由于是统计方案数的题,所以dp[0]=1,毕竟不点菜也是一种方案

状态转移方程

d[j]=d[j]+d[j-v[i]]

代码
#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma Gcc optinize("o1")
#pragma Gcc optinize("o2")
#pragma Gcc optinize("o3")
#pragma GCC optimize("Ofast")//输入优化 
using namespace std;
inline int read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快速读入 
int n,m,v[105];//记录每个拆分数的大小 
int dp[10010];//模拟被拆分的数的值 
int main(){
   n=read();
   m=read();
   dp[0]=1;//初始dp[0]=1,毕竟不选也是一种方案 
   for(int i=1;i<=n;i++){//模拟是第几个数 
       	v[i]=read();
     	for(int j=m;j>=v[i];j--){//模拟被拆分数的大小 
     		dp[j]=dp[j]+dp[j-v[i]];//状态转移方程 
		}
	}      	
   printf("%d",dp[m]);
   return 0;
}

写死我了,能看到这的都是大佬

我就是一名蒟蒻,如有不妥,还望改正

大佬们帮忙预测一下今年CSP-J有什么题呗

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值