动态规划
理论上来说贪心的速度是很快的:
(正常)到
(优先队列)
但是由于贪心太难以证明,子问题最有不一定全局最优等问题,故可以用一种全新的方法:
动态规划
当然,虽然它的时间复杂度是,但是以牺牲了一部分空间为代价的(
的空间复杂度)。
动态规划的使用条件
重复子问题:
存在子问题重复计算。
具备最优子结构:
后面的状态可以靠前面子问题推导。
无后效性:
已解子问题,不会受到后续决策影响。(跟贪心一样)
0/1背包
0/1背包用于:选n个不可分割的东西,求能选出的最大值,每个东西只选一次
例题
题面
洛谷P1048https://www.luogu.com.cn/problem/P1048
样例
输入
6 4
1 2
1 1
3 5
5 7
输出
9
解析
其实这道题可以用记忆化搜索,但鉴于为了抢最优时间,还是用0/1背包较好
模拟
主要是csdn显示不出来长表格,故只能用照片的形式
状态转移方程
代码:朴素版
空间复杂度
时间复杂度
代码
存储了数组的全部信息,就是上面的表格
#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;
}
代码:优化版
空间复杂度
时间复杂度
代码
存储了数组的全部信息,就是上面的表格
其实可以存在一个一维数组内,把选择第几号物品那一步删去,降低空间复杂度。但是由于需要左侧的值,故要从大到小逆序更新,保留左侧的值。
#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次
例题
题面
洛谷P1776https://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给滚掉
状态转移方程
代码:朴素版
空间复杂度
时间复杂度
可以发现,下面的代码可能会超时,故我们用二进制拆分
#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
数字 | 组法 |
---|---|
1 | 1 |
2 | 2 |
3 | 1+2 |
4 | 4 |
5 | 1+4 |
6 | 2+4 |
7 | 1+2+4 |
8 | 8 |
9 | 1+8 |
10 | 2+8 |
11 | 1+2+8 |
12 | 4+8 |
13 | 1+4+8 |
14 | 2+4+8 |
15 | 7+8 |
可以看出来,当你把一个数进行二进制拆分后,一定能用拆出来的数字组成小于等于被拆数字的数字
代码:优化版
空间复杂度
时间复杂度
#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个不可分割的东西,求能选出的最大值,每个东西可以选无数次
例题
题面
洛谷P1616https://www.luogu.com.cn/problem/P1616
样例
输入
70 3
71 100
69 1
1 2
输出
140
解析
我们可以去枚举到底能放多少个第i件物品,从而变成0/1背包问题
不过要注意的是如果还是要省略掉第1维的话,需要从小往大枚举
状态转移方程
代码
时间复杂度
空间复杂度
#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
例题
题面
洛谷P1833https://www.luogu.com.cn/problem/P1833
样例
输入
6:50 7:00 3
2 1 0
3 3 1
4 5 4
输出
11
解析
针对可以用无数次的物品采用完全背包;
针对可以用1次的物品和有限次数次的物品用多重背包;
故建立一个数组存储用哪一个背包
状态转移方程
因为要用两种背包,故有两种状态转移方程
完全背包
多重背包
代码
#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. 分组背包问题https://www.acwing.com/problem/content/description/9/
样例
输入
3 5
2
1 2
2 4
1
3 4
1
4 5
输出
8
解析
其实就是0/1背包的题,无非就是在加一层循环,枚举每一个组内的值
状态转移方程
代码
#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个数,每个数有的重量,
的体积,
的体积,要求在体积和重量都不超过要求的情况下,装下最多价值的物品
tips:状态不够,维数来凑
例题
题面
ACWing 8. 二维费用的背包问题https://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背包的写法差不多
状态转移方程
代码
时间复杂度
空间复杂度
#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 拆分成若干个自然数相加之和
例题
题面
洛谷P1164https://www.luogu.com.cn/problem/P1164
样例
输入
4 4
1 1 2 2
输出
3
解析
由于是统计方案数的题,所以,毕竟不点菜也是一种方案
状态转移方程
代码
#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有什么题呗