动态规划第一讲(01背包问题,完全背包为题,多重背包问题(1和2),分组背包问题)
参考www.acwing.com动态规划1
01背包问题
所有动态规划问题的核心都在于要明白两个问题,一是状态表示,二是状态计算。那么就从01背包问题开始讲。
先看例题:
原题链接:01背包问题
先审题,注意01背包的问题的特点:
- 每个物品只能使用一次
- 每个物品有无数个
然后从状态表示和状态计算两方面来分析解决问题,以下是y总上课讲课时的截图:
状态表示可以从两个方面入手,一个是集合,一个是属性。集合表示的是物品现在的状态。我们用*f[i,j]*来表示此时此刻背包的状态,只从前i个物品中选择,且总体积不超过j。然后物体的属性则是当前背包中物品的最大价值。
另一个就是状态计算了,个人理解,动态规划应该是一个递归的过程。所以我们需要划分每一个状态,01背包中的状态划分如图所示:状态划分为两个,一种为当背包容量为j的时候包含第i个物品,另一种为当背包容量为j但是不包含第i个的时候。
不包含第i个物品时就证明,加入第i个物品后,背包的总容量已经超出了j。当包含第i个物品时,为了方面求出背包的属性我们使用前一状态来计算此状态。所以在容量为j,且从前i个物品中挑物品的时候,背包的最大价值为max(f[i-1,j], f[i-1,j-vi]+wi。
下面上代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int weight[N];//表示每个物品的权重
int volume[N];//表示每个物品的体积
int f[N][N];//表示背包每一时刻的状态
int main(){
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> volume[i] >> weight[i];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
//这里的if判断是本人刚开始不理解的地方,后来通过把递归的过程图解了一遍理解了。
//背包的每个状态f[i,j],是根据上一状态提归来的,因为我们提前定义了f[i][j]这个
//二维数组,所以像f[0][1]=0,所以画过图后就可以知道。
if(j >= volume[i]){
f[i][j] = max(f[i-1][j], f[i-1][j-volume[i]]+weight[i]);
}
else{
f[i][j] = f[i-1][j];
}
}
}
cout << f[n][m] << endl;
return 0;
}
在二维状态写完后,要想着对代码进行优化。y总给出的优化方式是:将状态f[n][n]优化为一维数组。当优化为一位数组时我们一定要注意两个为题。问题1:当j<v[i]时,f[i][j] = f[i-1][j]。被优化为f[j] = f[j]。2.当作j>=v[i]时,这时一定要注意f[i][j] = f[i-1][j-v[i]]+w[i]。被优化为f[j] = f[j-v[i]]+w[i]。但是这个时候要注意,要倒叙遍历背包容量j,因为如果正序遍历的话,f[j]会提前被计算,而我们递归时要使用i-1时刻的状态而不是i时刻的状态,为了避免i-1时刻的状态不被提前计算,我们将使用倒序遍历。
下面上代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int weight[N];//表示每个物品的权重
int volume[N];//表示每个物品的体积
int f[N];//表示背包每一时刻的状态
int main(){
int n,m;
cin >> n >> m;
for(int i = 1; i<= n; i++) cin >> volume[i] >> weight[i];
for(int i = 1; i<=n; i++){
for(int j = m; j >= volume[i]; j--){
f[j] = max(f[j], f[j-volume[i]]+weight[i]);
}
}
cout << f[m] << endl;
return 0;
}
这里我推荐一个比较好理解的优化理解,也是来自acwing。点击01背包状态优化详解
完全背包问题
先上题目再分析完全背包问题
先审题:完全背包问题有两个特点
1.每个物品有无限个,不像01背包每个物品只有1个
还是先上y总的分析图
这里的状态表示跟01背包一样。唯一不同的是集合的划分。因为当数到第i个物品的时候,我们可以选择拿0个,1个,2个直到k个第i个物品。所以状态转移可以写为f[i][j] = f[i-kv[i]][j]+kw[i]。因为当k=0时就表示不取第i个物品。所以话不多说直接上代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N][N]; //每一时刻的状态
int v[N];
int w[N];
int main(){
int n,m;
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++){
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;
}
但是上述这种代码的时间复杂度过高为O(ijk)。所以我们采取跟01背包一样的方法,将背包状态f[N][N]从二维优化到一维。优化到一维时还是需要注意f[i][j] = max(f[i][j], f[i-1][j-k*v[i]]+w[i])时,f[i][j]的状态是根据f[i-1]得来的,所以注意f[i-1]的状态一定给不能被先计算,所以要倒叙遍历j。然后关于完全背包问题如果想去掉一层遍历非常的困难,所以有一些大神想出了一个特殊的优化方法
以下是楼主认为比较不错的解释
这里看出f[i][j]可以由f[i][j-v[i]]优化而来,因为每个物品都有无数个,所以最后两边的式子时对齐的。
话不多说上代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N];
int w[N];
int f[N][N];
int main(){
int n, m;
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++){
//这里为什么没有k,是因为每个f[i][j]都是由f[i][j-v[i]]根据不断地max()
//得来的,所以虽然有那么多k,但是每次都只max出来一个最大地
if(j >= v[i]) f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i]);
else f[i][j] = f[i-1][j];
}
}
cout << f[n][m] << endl;
}
如图所示,都已经优化到这里了,是不是很熟悉?对,这就时01背包的二维状态。所以完全背包理所当然的可以被优化成01背包的以为状态。
上代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N];
int w[N];
int f[N][N];
int main(){
int n, m;
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[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i]);
}
}
cout << f[n][m] << endl;
}
但是如果像上述这么写,肯定就错了!为什么呢,重新看状态转移图,01背包问题f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i]),确实使用的时i-1时候f的状态!但是大家注意看上面的图还有下面这张图
然后y总写出对比代码
这也是为什么当我们优化成01背包时,要从小往大遍历。
下面上代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N];
int w[N];
int f[N][N];
int main(){
int n, m;
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];//为什么要这么做,因为第一个集合是一定存在的!就是我不拿第i个物品
if(j>=v[i]) f[i][j] = max(f[i][j], f[i][j-v[i]]+w[i]);
}
}
cout << f[n][m] << endl;
}
注意f[i][j] = f[i-1][j]那里,因为这种状态一直存在(不拿第i个物品)
在优化时。
f[i][j] = f[i-1][j],这个状态可以优化为f[j] = f[j],等式在计算的时候也是先计算右边,所以右边还是f[i-1]的状态,所以是等价的。下面的f[i][j] = max(f[i][j], f[i][j-v[i]]+w[i]中前一项的f[i][j]其实就是f[i-1][j]
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N];
int w[N];
int f[N];
int main(){
int n, m;
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[j] = f[j];//为什么要这么做,因为第一个集合是一定存在的!就是我不拿第i个物品
if(j>=v[i]) f[j] = max(f[j], f[j-v[i]]+w[i]);
}
}
cout << f[m] << endl;
}
好了上述就是优化的完全背包的一维形式了。如果还看不懂,可以看看acwing上的完全背包优化为1维的详解
多重背包问题多重背包问题1
多重背包跟完全背包还有区别的。在多重背包问题中,每个物品有有限个。这种问题已经解过很多遍了直接上朴素版的代码了
#include<iostream>
#include<algorithm>
using namespace std;
//此方法为朴素算法解法
const int N = 110;
int f[N][N];
int v[N],s[N],w[N];
int main(){
int n,m;
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 = 1; 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;
}
切记当物品有有限个时,切不可用完全背包的方法来优化,因为完全背包问题中每个物品有有无限个,而多重背包问题中,每个物品的个数有限制
**
**
所以y总想出了用2进制的方法来优化。
优化代码地址:点击链接
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 12000;//因为一共有N=1000个物品,每个物品的个数为s=2000个,所以一共可以有12000个
int f[N];
int v[N];
int w[N];
int s[N];
int main(){
int cnt = 0;
int n,m;
cin >> n >> m;
//这里的方法就是把所有的物品全部打乱化成新的物品,因为二进制可以表示出任何s中所有物品的权值和体积。
for(int i = 1; i <= n; i++){
int volume, weight, s;
int k = 1;
cin >> volume >> weight >> s;
while(k <= s){
cnt++;
v[cnt] = k * volume;
w[cnt] = k * weight;
s = s -k;
k = k * 2;
}
if(s > 0){
cnt++;
v[cnt] = s * volume;
w[cnt] = s * weight;
}
}
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;
}
分组背包问题
分组背包问题
分组背包问题就是,每组有若干个物品,但是每组物品只能选一个。与完全背包的区别就是,完全背包是第i个物品选几个,而分组背包是第i个物品选哪个!但是在写这个代码的时候,楼主范了一个很严重的错误:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int f[N][N];
int s[N];
int v[N][N];
int w[N][N];
int main(){
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i];
for(int j = 1; j <= s[i]; j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
for(int k = 1; k <= s[i]; k++){
if(j >= v[i][k]) {
f[i][j] = max(f[i][j], f[i-1][j-v[i][k]] + w[i][k]);
}
else f[i][j] = f[i-1][j]
//为什么不能这样写是因为第i组物品里面只要求选一个。假设上衣状态f[i-1][j] = 2, 当j=4
//新状态s[1] = (1,1), s[2] = (4,5), s[3] = (5,2), s[4] = (3,10)
//k=1时 j > s[1] f[i][j] = max(2, 2+1) = 3
//k=2时 j < s[2] 所以f[i][j] = 2;这里就出现了问题了
//k=3时 j > s[3] f[i][j] = max(2, 2+5) = 7
//k=4时 j < s[4] f[i][j] = f[i-1][j] = 2
//这里就会出现大问题了,它会被更新回来
}
}
}
cout << f[n][m] << endl;
}
这样写是一个完全错误的写法!!!!以下写法才是正确的。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int f[N][N];
int s[N];
int v[N][N];
int w[N][N];
int main(){
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i];
for(int j = 1; j <= s[i]; j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
f[i][j] = f[i-1][j];//记住一定要在这里把状态做转移,因为s[i]中的物品我们只拿一件,根据状态划分我们不拿或者拿第k个
for(int k = 1; k <= s[i]; k++){
if(j >= v[i][k]) {
f[i][j] = max(f[i][j], f[i-1][j-v[i][k]] + w[i][k]);
}
}
}
}
cout << f[n][m] << endl;
}
然后优化成一维
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int f[N];;
int s[N];
int v[N][N];
int w[N][N];
int main(){
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i];
for(int j = 1; j <= s[i]; j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= 0; j--){
for(int k = 1; k <= s[i]; k++){
if(j >= v[i][k]) f[j] = max(f[j], f[j-v[i][k]]+w[i][k]);
//记住这里的if判断一定要单另写出来,如果写在for循环里面那么当有一个不满足时,后面满足条件的也不会被遍历!
}
}
}
cout << f[m] << endl;
}
完结,以后回来复习用