动态规划 01背包问题(简单易懂)

之前讲过了动态规划的几个例子,分别是动态规划 矩阵连乘问题 动态规划 最常公共子序列问题,学习了动态规划的使用,而01背包问题作为动态规划的经典问题,同时对贪心算法也是一个很重要的补充,所以也必须掌握01背包问题的原理和实现。

01背包问题

题目描述:

有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

分析这个问题,使用动态规划的话,我们首先想得是这个问题能不能分成子问题,然后优化子结构。

这里写图片描述

ps:动态规划自底向上,所以x轴是从 n-> 1,而y轴是从 1->m

看这个图,紫色部分name表示物品的名字,黄色部分weight表示物品的重量,绿色部分value表示物品的价值,蓝色部分表示承重从1到10的背包,白色部分当前承重的背包所能放入的物品的价值的最大值f[i, j]。

这里首先要说一个01背包状态转换方程:

f[i,j] = Max{ f[i+1,j-Wi]+Vi( j >= Wi ), f[i+1,j] }

  1. f[i,j]表示在前i件物品中选择若干件放在承重为 j 的背包中,可以取得的最大价值。
  2. Vi表示第i件物品的价值

这个式子现在不懂没关系,文章下会解释

问题分析

动态规划的要求就是从底向上,最后一行e,e的重量是4,价值为6,因此0-3的背包放不下,4-10的背包放得下,所以0-3背包的价值为0,4-10的背包价值为6。

然后从下往上看第二行,例如d4,表示承重为4的背包所能翻入d,e两个物品的价值的最大值,在这里承重4明显不能放入d,只能放入e,因为d的weight为5,e的weight为4,所以d4的最大值就是放入e时的价值最大值6,以此类推。

再来看a8 = 15,是怎么来的呢?按照公式,求a8则需要求 b6 + Va 和 b8,取其最大值,那么因为动态规划是自底向上的,所以当我们求a哪一行的时候,bcde行都已经求出,因此b6+Va = 15 > b8 = 9,所以a8应该等于15。

这个公式该怎么理解呢?或者说为什么f[i,j] 就一定会等于f[i+1,j-Wi]+Vi 和 f[i+1,j] 的最大值?
我们根据动态规划的思想来思考,使用动态规划,就是因为该问题可以通过分解为子问题的分治思想,求出各个子问题的最优子结构,从而得出我们问题的解。

a8(f[i][j]),表示承重为8的背包放入a-e物品的最大价值,
b6 + Va (f[i+1][j - w[i]] + Vi):表示我有一个承重为6的背包(原承重为8 减去 a的重量2),能放入bcde物品的最大价值,再加上a的重量的最大价值
b8 (f[i+1,j]):表示有一个承重为8的背包,能放入bcde物品的最大价值

这样考虑:我们求a8,用优化子结构的思想,先求预留了a的空间的背包的最大价值,再加上a的价值,不就是我需要求得的背包的最大价值了吗?

  1. 放入a之后,背包剩余的空间就是8-2 = 6,那么背包为6时的放入bcde物品的最大价值是9,再加上a的价值6等于15,得出我们承重为8的背包最大价值是15,但是还需要考虑下面这个原则:
    在同样的背包承重下,如果放入物品i的背包最大价值小于不放入物品i的背包最大价值,我们选择不放入该物品(因为价值都一样,放入了反而占空间,不放此物品我还可以放其它物品)

  2. 因此我们还要考虑不放入物品a的情况即b8 (f[i+1,j]),b8 = 9,明显放入a之后价值为15 大于 不放入时的价值9,所以我们选择放入

总结:
由以上分析可得,a8=15

代码实现

#include<iostream>
#include<algorithm>

using namespace std;
const int n = 5;//表示物品的数量 
const int m = 10;//表示背包所能承受的重量,从1-10 
int weight[n+1] = {0, 2, 2, 6, 5, 4};//物品的重量,前面0下标为0只是为了让下标对齐 
int value[n+1] = {0, 6, 3, 5, 4, 6};//物品的价值 
int f[n+1][m+1];//表示能承受重量为j的背包放入1-i物品的最大价值 

void package01(){
    int i = n, j;
    //首先对最底下的进行填充 
    for(j = 1; j <= m; j++){
        if(j < weight[i]){
            f[i][j] = 0;
        }else{
            f[i][j] = value[i]; 
        }
    }

    //然后对剩下的n-1个物品填充
    for(i = n -1; i > 0; i--){
        for(j = 1; j <= m; j++){
            if(j < weight[i]){
                f[i][j] = f[i+1][j];
            }else{
                f[i][j] = max(f[i+1][j-weight[i]] + value[i], f[i+1][j]);
            }
        }
    }

    for(i = 1; i <= n; i++){
        for(j = 1; j <= m; j++){
            cout << f[i][j] << " ";
        }
        cout << endl;
    }

    cout << "承重为10的背包最大价值是:" << f[1][10];
}

int main(){
    package01();
}

Java版:

public class Package01 {

    /**
     * 01背包状态转移方程:f[i,j] = Max{ f[i+1,j-Wi]+Vi( j >= Wi ), f[i+1,j] }
     * i 表示第几个物品,j表示背包的重量
     */

    public void sovle(){
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();//表示背包的最大重量
        int[] w = new int[n]; //表示每一个物品的重量
        int[] v = new int[n]; //表示每一个物品的价值
        int[][] f = new int[n][m+1]; //用来表示状态转移方程
        for (int i = 0; i < n; i++) {
            w[i] = sc.nextInt();
        }
        for (int i = 0; i < n; i++) {
            v[i] = sc.nextInt();
        }

        //自底向上
        //首先构造底部背包
        int i = n-1, j = 0;
        //首先对最底下的进行填充
        for(j = 1; j <= m; j++){
            if(j < w[i]){
                f[i][j] = 0;
            }else{
                f[i][j] = v[i];
            }
        }

        //然后对剩下的n-1个物品填充
        for(i = n-2; i >= 0; i--){
            for(j = 1; j <= m; j++){
                if(j < w[i]){
                    f[i][j] = f[i+1][j];
                }else{
                    f[i][j] = Math.max(f[i+1][j-w[i]] + v[i], f[i+1][j]);
                }
            }
        }

        for(i = 0; i < n; i++){
            for(j = 1; j <= m; j++){
                System.out.print(f[i][j] + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        new Package01().sovle();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值