动态规划之背包问题

前言:

动态规划的本质,是对问题状态的定义和状态转移方程

动态规划具备三个特点:

        1.将原来的问题分解成几个相似的子问题;

        2.所有的子问题都只需要解决一次

        3.每个状态存储子问题的解

一般从三个角度考虑动态规划:

        1.状态表示:

        2.状态计算 - > 集合的划分

        3.状态初始化

集合的划分依据,需要满足两个条件

        1. 以最后一个改变的元素为依据。

        2. 集合中应包含所有的方案。

而动态规划问题一般可以分为线性DP,背包问题,区间DP,计数类DP,数位统计DP,状态压缩DP,树形DP,背包问题是大头,也是我们这章的重点。

全文共12499

目录:

一 .四个基础背包问题 

        ①01背包        

        ②完全背包

        ③多重背包

        ④分组背包

二 .背包问题的扩展

        ①二维背包

        ②混合背包

        ③背包求方案数

        ④背包存路径

        ⑤背包问题求方案数的至多、至少、恰好问题的总结

正文:

.背包问题都以01背包为基础

我们以01背包问题为引子:

有 N 件物品和一个容量是 V 的背包。问最多能装入背包的总价值是多大?

状态表示所有只从前i个物品中选, 总体积不超过j的方案集合

集合划分

初始化:第0行和第0列都为0,表示没有装物品时的价值都为0:F(0,j) = F(i,0) = 0

对于集合的两种划分方式:

第一种方案:如果不选择第i个物品,价值就与前一维相同: f[i][ j] = f[i - 1][j] 

第二种方案:不选择第i个物品: f[i][j] = f[i - 1][j - v[i]]  + w[i] 

从而得到状态转移方程,即max(f[i][j], f[i - 1][j - v[i]]  + w[i] )

问题一:为什么方案二在这里第二维表示是j - v[i] ? 

答:因为要选择的第i个物品体积为v[i] ,而总体积为j,所以要空出v[i]的体积来填充

二维c++代码如下:时间复杂度 :O(n * m )

注:具体题目可以在洛谷、力扣等平台找

#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]; //注意i要大于0,因为后面要用到下标i - 1;
    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= m; j++) {
            if(j < v[i])f[i][j] = f[i-1][j];
            else f[i][j] = max(f[i - 1][j], f[i-1][j-v[i]]+w[i]);
            //最后即比较加上该物品时的价值与不加该物品时的价值哪个大, 
            //不加该物品时f[i][j]是指不包含该物品时其它相加的价值总和
            //而加上此物品后,得到的是 f[i][j - v[i]] 的价值最大值加上当前物品价值w[i]
        }
    }

    cout << f[n][m] << endl;
 return 0;    
}

为什么答案是f[n][m] ? 

答:第一维是因为要枚举n个物品, 而第二维,即我们的背包体积,f[n][m] 是从f[1][0]一路推过来的。 

一维优化: 时间复杂度 :O(n * m )

​
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;

int n, m;//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 --)//如果正序枚举,j循环中的小体积的更新更新会 影响后面的更新 
            f[j] = max(f[j], f[j - v[i]] + w[i]);//因为f[j - v[i]] + w[i]这里用的 f[j - w[i]
                                                //应该是上一层i的,即i - 1而不能在这层之前被更新,否则会影响到后面更新       

    cout << f[m] << endl;
    return 0;
}


​

这里存在两个问题:

一. 为什么可以这样优化 

        我们可以注意到二维数组的更新方式为

        f[i][j] = f[i - 1][j]  // 不含i的所有选法的最大价值
        if j >= v[i]    //判断含i的选法是否成立
        f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])

        可以看出,f[i][] 只与f[i - 1]相关,所以根本没必要保留f[i - 2]即后面的状态值。       

        将空间从O(n*m)优化为 O(m), n, m分别为物品个数和背包体积。

二. 为什么第二行循环需要倒叙枚举 (与之相对的是完全背包的正序枚举)

        当我们更新索引值较大的dp值时,需要用到索引值较小的上一层dp值dp[j - v[i]];
也就是说,在更新索引值较大的dp值之前,索引值较小的上一层dp值必须还在,还没被更新;
所以只能索引从大到小更新。 

就是说 在状态转移方程f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])中f[i - 1][j - v[i]] 这里使用的是i - 1层的dp,而如果我们正向枚举时 我们的 f[ j - v[i] ]会在 f[j] 之前更新,也就是在更新到f[j - v[i]]这一层时,我们使用的将是被更新过的 f[i][j - v[i]] ,这与原方程f[i - 1][j - v[i]] 不同,所以我们需要倒叙枚举,使得我们将使用的f[j - v[i]]  是未被更新过的

二.有了以上的基础,我们就可以展开对 完全背包,多重背包, 分组背包, 这三个基础背包模型的学习

   完全背包有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。

   状态表示: 所有只从前i个物品中选, 总体积不超过j的方案集合

   集合的划分方式:第i个物品选几个

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值