终于来到了动态规划问题。
01背包问题
背包问题属于动规问题中一个非常大的门类,首先要推荐的就是背包九讲,可以说是必读的一个系列博客。
01背包
特点:每个物品仅能使用一次
重要变量&公式解释:
f[i][j]
:表示所有选法集合中,只从前i个物品中选,并且总体积≤≤j的选法的集合,它的值是这个集合中每一个选法的最大值.
状态转移方程:f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i-1][j]
:不选第i个物品的集合中的最大值
f[i-1][j-v[i]]+w[i]:
选第i个物品的集合,但是直接求不容易求所在集合的属性,这里迂回打击一下,先将第i个物品的体积减去,求剩下集合中选法的最大值.
问题:集合如何划分
一般原则:
- 不重不漏,不重不一定都要满足(一般求个数时要满足)
- 如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来.
无优化版代码:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];//体积、价值
int f[N][N];// f[i][j]表示的状态是:j重量下前i个物品的最大价值
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 = 0; j <= m; j++)//关于下标的起始,要思考是否合法即可,比如j = 0,就是总体积是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]);
}
cout << f[n][m] << endl;
return 0;
}
将二维优化成一维如下:
/*
1. f[i] 仅用到了f[i-1]层,
2. j与j-v[i] 均小于j
3.若用到上一层的状态时,从大到小枚举, 反之从小到大
*/
#include <iostream>
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[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i])
;//01背包
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i])
;//完全背包问题
最朴素的写法(数据加强后已经不能ac了)
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][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 = 0; j <= m; j++)
for (int k = 0; k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] <<endl;
return 0;
}
分析过状态转移后优化的朴素写法:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][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 = 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] << endl;
return 0;
}
最终优化后的代码:
#include <iostream>
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 = v[i]; j <= m; j++)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
多重背包问题 I
只需要在完全背包的基础上加上 k <= s[i]
的判断即可。
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main ()
{
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]);
cout <<f[n][m] << endl;
return 0;
}
多重背包问题 II
思路和多重背包问题I一样,但这题的数据范围变成1000了,非优化写法时间复杂度O ( n 3 ) O(n^3)O(n3) 接近 1e9必超时。所以需要用二进制优法。
假设有一组商品,一共有11个。我们知道,十进制数字 11 可以这样表示
11 = 1011 ( B ) = 0111 ( B ) + ( 11 − 0111 ( B ) ) = 0111 ( B ) + 0100 ( B ) 11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)。正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。
现在,如果我们把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “, 将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。
这种优化对于大数尤其明显,例如有1024个商品,在正常情况下要枚举1025次 , 二进制思想下转化成01背包只需要枚举10次。
#include <iostream>
#include <algorithm>
using namespace std;
//2的12次方大于2000,也就是说一个数最多可以拆成12个,故数组容量乘12
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[M];
int main()
{
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] << endl;
return 0;
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];//v为体积,w为价值,s代表第i组物品的个数
int f[N];//只从前i组物品中选,当前体积小于等于j的最大值
int main()
{
cin >> n >> m;
//读入
for (int i = 1; i <= n; i ++ )
{
cin >> s[i];
for (int j = 0; j < s[i]; j ++ )
cin >> v[i][j] >> w[i][j];
}
//若用到上一层(i - 1)的状态时,从大到小枚举;用到当前状态则从小到大枚举
for (int i = 1; i <= n; i ++ )//从前往后枚举每一组
for (int j = m; j >= 0; j -- )//从大到小枚举所有体积
for (int k = 0; k < s[i]; k ++ )//枚举所有选择
if (v[i][k] <= j)//第i组的第k件物品的要能放得下背包,才去更新状态
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}