栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

动态规划之01背包问题

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

动态规划之01背包问题

目录

问题描述

问题分析

朴素想法

动态规划——二维数组

重要优化——一维滚动数组

写在最后


问题描述

2. 01背包问题 - AcWing题库

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 0

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

问题分析

01背包是一种最基本的背包问题,特点是背包容量有限,每个物品有且仅有一个。对于每个物品,都有放与不放两种选择,因此也叫01背包问题。

朴素想法

最朴素的想法是依次枚举每件物品选与不选的情况,时间复杂度为显然是低效而不可取的。但这也给了我们拆分重叠子问题的思路:从1到n依次考虑物品的状态,通过构造最优子结构来求得最后的解。

怎么表示每件物品的状态呢?

动态规划——二维数组

我们最先想到的是用一个二维数组表示状态,其中表示前i个物品放进容量为的背包能够获得的最大价值。

依次考虑物品状态,怎么列出状态转移方程求解呢?

考虑对第i件物品的选择策略,有以下两种:

不放第i件物品,那么显然有;放第i件物品,问题转化为

从而得到状态转移方程:

进而可以写出以下代码:

#include 
#include 
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int dp[N][N];
int main(){
    ios::sync_with_stdio(false); //关闭流同步,提高cin速度
    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 ++){
            dp[i][j] = dp[i - 1][j];
            if(j >= v[i]) //判断能不能装下
                dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
        }
        cout << dp[n][m] << 'n';
        return 0;
}

重要优化——一维滚动数组

可以知道,时间复杂度和空间复杂度都是,其中时间复杂度无法优化,空间复杂度却可以进一步优化。

经过思考可以发现,原先方法计算的时候总是要用到这个维度中,在左侧的数组元素,而当计算这一维度的时候,只需要用到这一维度,这一维度则完全用不到了。因此,不妨直接开一个一维数组(即省去第一维),代表状态为:

N件物品,背包容量为j下的最优解。

这样,状态转移方程改变为:

注意点:在枚举j的时候,需要逆序从右往左枚举。这是01背包最难以理解的地方。

在这里需要引入滚动数组的概念:

在求解斐波那契数组的时候,我们通常觉得要开一个大数组(比如说),表示的就是第i个元素的值;但是当我们仅仅需要第k个元素的值的时候,事实上我们不关心其他元素的值是多少;那么,来来回回我们真正需要存储的值只有三个:当前元素的值,上一个元素的值,上上一个元素的值。有了这三个,我们就可以循序渐进,求出后面元素的值,当求出下一个元素的值之后,上上一个元素其实已经对我毫无用处了,不如舍弃它,拿它的下一个元素覆盖它;此时,“当前元素”变成了“上一个元素”,“上一个元素”变成了“上上一个元素”,可以进一步求值。我们向前推进了问题,数组却还是原来的数组;那么,我就可以开一个大小为3的数组(或者直接定义三个变量,本质上是一样的),不断地用后面的运算结果覆盖前面的运算结果,最后求得答案。

def fibonacci(n):
    a = 1
    b = 1
    ans = 0
    if n == 1 or n == 2:
        return a
    else:
        while n >= 3:
            ans = a + b
            a = b
            b = ans
            n -= 1

        return ans


n = int(input())
print(fibonacci(n))
//懒得写C++了,附上翻出的python代码

这就是滚动数组,不断地用后面的值覆盖前面的值,车轮滚滚向前,重复利用着每一寸轮胎。

这正是动态规划中常用的技巧。再来看原来的问题:为什么需要从后往前枚举?

我们对比一下两个状态转移方程:

 

当前的问题是:背包容量为j时选N件物品的最优解

在二维解法中,我们当前这一维度的状态是由这一维度得来的;在一维解法中,的状态是由自身与左边的元素得来的;二维状态与一维状态的状态转移方程,本质是一样的,这就是说对于一维状态下左边的元素,它们在二维解法中实际的第一维度是;如果我们按正序遍历数组,在处理当前问题的时候,我们用到的来自左边的元素实际的第一维度是,因为已经被更新过了;这就体现了逆序遍历的合理性:从右向左遍历,使得右侧被更新过的、需要留给下一个维度的数据不会再被使用;而左侧需要使用的数据都还没被更新,依然来自上一个维度。

一切如上。接着来看代码:

#include 
#include 
using namespace std;
const int N = 1010;
int dp[N], v[N], w[N];
int main(){
    ios::sync_with_stdio(false); //关闭流同步,提高cin速度
    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 --){
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
        cout << dp[m] << 'n';
        return 0;
}

这里有最终优化版本

#include 
#include 
using namespace std;
const int N = 1010;
int dp[N], v, w;
int main(){
    ios::sync_with_stdio(false);
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
        cin >> v >> w;
        for(int j = m; j >= v; j --)
            dp[j] = max(dp[j], dp[j-v] + w);
    }
    cout << dp[m] << 'n';
    return 0;
}

写在最后

动态规划如何避免重复计算d的问题在01背包问题中非常明显。暴力枚举方法忽略了这一点:在考虑第件物品选与不选而产生的最大值的时候,它其实是由前面件物品的最大值决定的。

因此,01背包的每件物品其实都可以看做一个阶段,这阶段里的状态有,它们均由上一个阶段得到。对于能够划分阶段的问题来说,可以尝试把阶段作为状态的一维,这样能使我们更加方便地得到满足无后效性的状态。如果当前设计的状态不能满足无后效性,可以尝试升维操作。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/717415.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号