acwing动态规划第一讲笔记

动态规划第一讲(01背包问题,完全背包为题,多重背包问题(1和2),分组背包问题)

参考www.acwing.com动态规划1

01背包问题

所有动态规划问题的核心都在于要明白两个问题,一是状态表示,二是状态计算。那么就从01背包问题开始讲。
先看例题:
原题链接:01背包问题
在这里插入图片描述
先审题,注意01背包的问题的特点:

  1. 每个物品只能使用一次
  2. 每个物品有无数个

然后从状态表示和状态计算两方面来分析解决问题,以下是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;
    
}

完结,以后回来复习用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值