zoukankan      html  css  js  c++  java
  • 动态规划——01背包问题

    一、最基础的动态规划之一

    01背包问题是动态规划中最基础的问题之一,它的解法完美地体现了动态规划的思想和性质。

    01背包问题最常见的问题形式是:给定n件物品的体积和价值,将他们尽可能地放入一个体积固定的背包,最大的价值可以是多少。我们可以用费用c和价值v来描述一件物品,再设允许的最大花费为w。只要n稍大,我们就不可能通过搜索来遍查所有组合的可能。运用动态规划的思想,我们把原来的问题拆分为子问题,子问题再进一步拆分直至不可再分(初始值),随后从初始值开始,尽可能地求取每一个子问题的最优解,最终就能求得原问题的解。由于不同的问题可能有相同的子问题,子问题存在大量重叠,我们需要额外的空间来存储已经求得的子问题的最优解。这样,可以大幅度地降低时间复杂度。

    有了这样的思想,我们来看01背包问题可以怎样拆分成子问题:

    要求解的问题是:在n件物品中最大花费为w能得到的最大价值。显然,对于0 <= i <= n,0 <= j <= w,在前i件物品中最大花费为j能得到的最大价值。

    可以使用数组dp[n + 1][w + 1]来存储所有的子问题,dp[i][j]就代表从前i件物品中选出总花费不超过j时的最大价值

    可知dp[0][j]值一定为零。那么,该怎么递推求取所有子问题的解呢。显而易见,要考虑在前i件物品中拿取,首先要考虑前i - 1件物品中拿取的最优情况。

    当我们从第i - 1件物品递推到第i件时,我们就要考虑这件物品是拿,还是不拿,怎样收益最大。

    ①:首先,如果j < c[i],那第i件物品是无论如何拿不了的,dp[i][j] = dp[i - 1][j];

    ②:如果可以拿,那就要考虑拿了之后收益是否更大。拿这件物品需要花费c[i],除去这c[i]的子问题应该是dp[i - 1][j - c[i]],这时,就要比较dp[i - 1][j]和dp[i - 1][j - c[i]] + v[i],得出最优方案。

    细节和代码如下:

    int n, w;
    int c[maxn], v[maxn];//c为费用,v为价值
    int dp[maxn][maxw];
    
    int main()
    {
        scanf("%d %d", &w, &n);//w为最大费用,n为数量
        for(int i = 1;i <= n;i++)
        {
            scanf("%d %d", &c[i], &v[i]);//输入,注意这里的下标是从1开始的
        }
        memset(dp, 0, sizeof(dp));//若不涉及多组输入,这一步其实可以省略
        //如果下标从0开始,下面也需要稍作修改
        for(int i = 1;i <= n;i++)
        {
            for(int j = 0;j <= w;j++)
            {
                if(c[i] > j)
                    dp[i][j] = dp[i - 1][j];//状态转移,情况①
                else
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + v[i]);//情况②
            }
        }
        printf("%d
    ", dp[n][w]);
    }

    这样做,时间复杂度和空间复杂度都是o(nw)

    二、空间优化 一维数组实现

    在上面的解法中,我们把所有子问题的最优解都用数组保存了下来,实际上,这些最优解并不是一直都有用的。从状态转移方程可以看出,对当前的i,只有i - 1时的最优解才是有用的,再之前的已经不会再被使用了。如果能把已经无用的空间节省出来,空间复杂度能够得到非常大的优化,这在有些问题中是非常必要的。

    实际上,只需要一个一维的数组dp[m + 1](省去n那一维)就可以完成任务。外层的循环每一轮开始时,这个数组里存的要么是初始值(i = 0的情况),要么是上一轮递推得到的值(i - 1时的情况),符合原本状态转移方程的要求。

    不过,这样做就需要更加的注意递推的方向,更新的顺序。在更新前,数组里存储的是上一轮的值(或初始值),更新后,更新了的位置存储的就是这一轮的值了。在01背包的问题里,我们要从i - 1的情况递推上来,所以要倒着更新。这一点需要着重理解(反了的话就变成完全背包了)。

    代码如下:

    int c[maxn], v[maxn];//c为费用,v为价值
    int dp[maxw];
    //其余部分略
    for(int i = 1;i <= n;i++)
    {
        for(int j = w;j >= c[i];j--)
        //这里要反着更新,否则dp[j - c[i]]会比dp[j]先更新,而更新后它对应的就不是i - 1时的状态了
        {
            dp[j] = max(dp[j], dp[j - c[i]] + v[i]);
        }
    }

    三、01背包的常见变形

    实践中很难遇到如此标准的01背包模型,大多数情况下,我们都需要根据具体问题对上面的算法做出一定的修改。

    这些问题都是背包问题常见的,解决方法也都相同或者相似:

    1.求取的不是价值的最大值而是最小值:把max换成min即可,原理相同。

    2.费用不一定都是正数:负数没法用作数组的下标,可以考虑把所有的费用都先加上一个较大的数再做处理(平移)。

    3.费用存在小数:可以把所用费用都先放大一定的倍数,是所有的费用值都为整数(推荐HDU 1864)。

    4.要求恰好装满,即选取的物品的费用之和恰等于最大花费:

    相比基础的01背包,这里我们需要一个正常情况不可能出现的值来表示状态非法、不可能实现,这个值可以是-1、-INF之类的,视具体情况而定。

    在初始化时,除dp[i][0]的值应为0之外,其他所有值都应为非法值。在状态转移时,首先要判断子问题是否非法。

    for(int i = 1;i <= w;i++)//初始化
    {
        dp[i] = -INF;
    }
    dp[0] = 0;
    for(int i = 1;i <= n;i++)
    {
        for(int j = w;j >= c[i];j--)
        {
            if(dp[i - c[i]] != -INF)//判断是否合法,这里其实省去了几种情况,用二维数组实现的话需注意
            {
                dp[j] = max(dp[j], dp[j - c[i]] + v[i]);
            }
        }
    }
  • 相关阅读:
    React元素渲染
    初识JSX
    微信小程序复制文本到剪切板
    微信小程序报错request:fail url not in domain list
    小程序,通过自定义编译条件,模拟推荐人功能
    积分抵扣逻辑
    微信小程序 switch 样式
    tomcat 配置开启 APR 模式
    tomcat8 传输json 报错 Invalid character found in the request target. The valid characters are defined in RFC 3986
    c++数组初始化误区
  • 原文地址:https://www.cnblogs.com/sun-of-Ice/p/9431351.html
Copyright © 2011-2022 走看看