背包问题就是陪你看花开向阳

2024-05-03 11:58
文章标签 问题 背包 向阳 看花开

本文主要是介绍背包问题就是陪你看花开向阳,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载原文是http://www.cnblogs.com/forever97/p/bagproblem.html

 这个是专门讲01背包问题

http://blog.csdn.net/mu399/article/details/7722810

自己看懂了的


一开始接触背包问题,总会有些困扰,无法完全理解,或者说不断地忘记所谓的公式,特神说分享,可以让许多人少走弯路,我觉得颇有道理,其实我想做的,只是努力让让算法变得看上去简单易懂一些,因为我也曾是个迷惘无助,没有方向的初学者。

  才疏学浅,言语颇显幼稚,如有纰漏请指教。

  允许我摆脱背包单调的题面,给题目一些我喜欢的模样:

 

01背包问题

  浙工大的向日葵地现在需要种植一些新的向日葵,现在有一些向日葵,你知道每株向日葵需要占用的位置大小,以及它们能产生的美丽值,告诉你最多能占用的位置。

  如果没有接触过背包问题,可能有些人会有性价比的思想,就像淘宝购物时都会货比三家。

  这种思想很单纯,也很符合常理。将美丽度除以占用位置大小,按照比值排序,这样子,每次就可以选到性价比最高的向日葵,这种想法在ACM中也有一席之地,我们称之为贪心算法。

  但是显然,有些情况下,这是不成立的,为什么呢?因为空缺位置的浪费。如果有刚好可以填满位置的高性价比向日葵那么当然这是最优解,但是如果按照比值排序选取一些最优向日葵后,剩下的空位,放不下别的向日葵,但是别的比值较低的向日葵组合选取,可以获得更多的美丽值,当然后者的做法可以让这片向日葵田更加地美丽。就像,“买买买”也不是什么好事情,做出正确的选择,更为重要。

  那么如何去正确选择需要种植的向日葵呢?答案是动态规划。动态规划的特点在于考虑每个阶段的最优情况。每个状态是从上一个状态中获取,而不是凭借单个的最优情况自已为是地作出错误判断。

  现在给出状态方程,记dp[i][j]给出空位置大小为j,在对第i株向日葵做出决策时的最大美丽值,w[i]为第i株向日葵的位置占用,v[i]为第i株向日葵所能够产生的美丽值。

  能够得到动态规划方程,dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]),就是每一次,对于不同的已有情况,选取新的向日葵是更优的,那么就保留这个更优值,否则,保留原来的值,那么对于每个dp[i-1][j],在对第i株向日葵判断时,都是最优的,那么所得到的dp[i][j]也均是最优的,最后得到的dp[i][j],也必然是最优的,最后获得的dp[i][j]中的最大值,当然就是最大的美丽值了。

  那么,这个方程一定要占用那么大的空间么?答案是,不需要。

  我们从这个方程可以得到,每次dp[i][j]都是从dp[i-1][j]中推导出来的,那么之前所记录的dp[0..i-2][j]在当前的步骤都是没有作用了的,那么,所占用的空间便可以节省了,所以最后得到的核心代码是

?
1
2
3
for ( int i=1;i<=n;i++)
     for ( int j=V;j>=w[i];j--)
         dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

  最后dp[0..V]中的最大值就是答案,那么为什么j的循环是倒序的呢,我想一定有人有这样的困惑,那是因为每株向日葵是独一无二的,在一种情况中只可以被选取一次,如果正序的话,因为每个dp[j]是从dp[0..j-1]中推导出来的,那么在从前往后推导的过程中,会出现反复选取同一株向日葵的情况了,当然,这也是一种题型,在后文中会提到。

  那么为什么答案不是dp[V],而是dp[0..V]呢?因为,位置占用刚好的时候未必是最大值,或者更糟糕的时候,无法占用全部的位置。但是如果说,我们要用向日葵恰好填满所有空缺,dp[V]就是最终答案了,差之毫厘,就是完全不同的情况。

  当然在这段核心代码和下文的代码中需要注意dp[i][j]的初始值都为0。

  以上就是所谓的01背包的模型和其优化,当然,美丽值这种东西我随便写写,美丽是不可以被丈量的,艺术也一样,一个人的努力更是。这个社会的浮夸所导致的结果主义,我们未必需要跟随。

完全背包问题

  现在,让我们来换一种情况,向日葵不是单株给出所占位置大小和美丽值,而是现在有许多不同种类的向日葵,我们来选取一些,使得美丽值最大。

  让我来再明确一下这种情况和刚才那种情况的差别,就是每种给出的向日葵是可以无限使用的,要是你喜欢这种样子的向日葵,你不用担心它只有一株,你可以一直去用这种向日葵来装点这个校园,那么也许你猜到了动态规划的核心代码,没错,就是第一个给出的核心代码把循环改为正序后产生的代码

?
1
2
3
for ( int i=1;i<=n;i++)
     for ( int j=w[i];j<=V;j++)
         dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

   这种情况,就是完全背包类型的题目了。

  是不是对背包系列的问题有了那么一些感觉呢?

多重背包问题

  那么趁热打铁,再来看一种新的情况,我们对第二种情况加一个限制条件:每种种类的向日葵只能选取有限的个数,这个时候应该怎么做呢?

  用c[i]表示第i种向日葵最多可以选取的次数。

  其实很简单,在01背包的核心代码加入一重循环,即增加选取该向日葵的株数c[i]的循环,就可以完成这种类型的正确选取:

?
1
2
3
4
for ( int i=1;i<=n;i++)
     for ( int j=V;j>=w[i];j--)
         for ( int k=1;k<=c[i];k++)
             if (k*w[i]<=j)dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);

  那么一定要每种情况都枚举么?一种类型的向日葵选取1株,2株,3株都尝试过去是否过于麻烦?这个时候二进制拆分的思想就可以起优化作用了,我们可以将向日葵的可选株数,进行拆分,比如7=4+2+1,那么在完全背包的模式下,只要枚举4,2,1三种情况,就可以了,因为在枚举选取两株是否可行的时候,会调用到之前的1株情况,那么就可以产生选取1,2,3株的不同情况下的最优值,当4被枚举到的时候,1至7株的情况就全部被枚举过去了,这个优化在执行的时候,效果还是很显著的,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
for ( int i=1;i<=n;i++){
     for ( int k=1;k<=c[i];k<<=1){
         for ( int j=V;j>=k*w[i];j--)
             dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
         c[i]-=k;
     }
     if (c[i]){
         for ( int j=V;j>=c[i]*w[i];j--)
             dp[j]=max(dp[j],dp[j-c[i]*w[i]]+c[i]*v[i]);
     }
}

  这种背包模型就是多重背包了,二进制拆分处理的时候注意最后剩余的部分不要忘记。

二维费用背包问题

  接下来,再看一下更为复杂的问题,如果,向日葵不仅需要占用有限的位置,还需要占据土地中有限的水资源,这个时候应该如何选取向日葵使得美丽值最大呢?也许理解了上面几种情况,你可以很容易地想出方法,增加一重循环,同时dp时增加一维,这样就可以完成这个任务了。代码如下:

?
1
2
3
4
for ( int i=1;i<=n;i++)
     for ( int j=w[i];j<=V;j++)
         for ( int k=u[i];k<=W;k++)
             dp[j][k]=max(dp[j][k],dp[j-w[i]][k-u[i]]+v[i]);

  上面的代码明显是二维费用完全背包的核心代码,至于01背包和多重背包我想应该可以轻易类推出来。

分组背包问题

  现在,增加一些有意思的元素,采购向日葵的时候,店家说,我这些向日葵都有一个分组,你所选取的向日葵都分别从属于不同的组,这样子搭配才美丽,也就是说,现在,在每个组中,我们至多只能选取一株向日葵,然后在这个前提下,使得向日葵的美丽值最大。

  先来看一下核心代码:

?
1
2
3
4
for 所有的组k 
     for j=V..0
         for 所有的i属于组k
             dp[j]=max(dp[j],dp[j-w[i]]+v[i]);

  在这个问题中,最为重要的就是如何处理在一个组中只选取一株向日葵的问题,我们对比一下01背包的核心代码和现在的核心代码,微妙的差别就在于循环的前后位置,01背包是在枚举每株向日葵的时候,枚举不同的位置分配,而现在是在枚举不同的位置分配的时候,枚举每个组的向日葵。因为先枚举分配的位置大小可以防止在同一个位置分配的最优情况中,同一组的向日葵有多个被选中。现在这个循环的顺序可以避免这种情况的出现。上述模型就是所谓的分组背包问题了。

树形背包问题

   接下来来看另一种情况,如果店家说,有些向日葵必须要在种植了一些向日葵后种植才会好看,不然就显得很糟糕,现在告诉你他们的依赖关系,请你选取一些向日葵使得他们的美丽值最大,这种情形就比较麻烦了,就像技能树一样,要开启上面的技能,才能够继续开启下面的技能,这种问题的解决方法,就是树形DP,首先根据依赖情况构建一棵树,你可能会说,那不是森林么?因为有些物品没有依赖关系呀。这个时候我们就建立一个虚根节点,将所有的树根作为它的儿子节点。

  做法是,在DFS的同时,进入每个儿子节点计算在不同分配位置大小的情况下获得的最大美丽值,最后上传最优值。注意在进入每个儿子节点的时候,该节点是必须选取的。可能这么说还是让人很迷糊,那么还是看一下搜索的核心代码再结合理解一下,会比较好:

?
1
2
3
4
5
6
7
8
9
void dfs( int x){
     vis[x]=1;
     for ( int i=w[x];i<=m;i++)dp[x][i]=v[x];
     for ( int i=1;i<=n;i++) if (vis[i]==0&&map[x][i]){
         dfs(i); for ( int j=m;j>=w[x];j--)
         for ( int k=1;k<=j-w[x];k++)
         dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[i][k]);
     }
}

  在代码中,我用map这个数组记录了x和i节点是否是父子关系,同时标记节点是否被访问,减少运算量。那么搜索一遍根节点就可以获得这个问题的最优值了。这种问题,我们称为树形背包问题。

 

  背包问题的种类并不是我不断改变选取向日葵的方式就可以枚举得完的,关键是如何在掌握了基础的几种背包问题之后,在一个复杂的问题中能够快速找到模型,针对问题想出解决方案,这是一个ACMer所需要拥有的素质。

  前段时间,一些事情闹的很不愉快,很多人把ACM说的一无是处,其实,我并不反对算法无用论,但是,ACM所给我带来并不仅仅是算法和数据结构而已,而是,在ACM这个圈子里,我很开心可以认识许多积极向上,对生活充满信心,即使被难题所困扰,仍然不会放弃希望的人,那些,并不能被所有人看到的东西,弥足珍贵。我深为自己身体里所流淌着的一些激昂而温和的情感,感到骄傲。

  好像,开始一些伤感的话题了,我们回归正题,来看几道ACM的题目,来更好地理解一下背包问题的运用:

HDU - 5410 CRB and His Birthday 

  题目大意:CRB生日,妈妈要给他买礼物,妈妈有M元钱,这家店有N种礼物,因为店长和妈妈是熟人,所以若第i种礼物买x件的话,店长会给妈妈Ai*x+Bi颗糖果,现给出每种礼物的单价、Ai值与Bi值,问妈妈最多能拿到多少颗糖果。

  题解:这是刚在暑假多校联赛时做过的题目,很显然这是一种背包问题,关键在于处理第一个特殊的礼物,这就相当于是一种特惠,跟现在的淘点点每日首单打折的差不多,b[i]的贡献仅在于买该种礼物的第一件,所以引入一维参数,表示是否买过这个物品,然后做完全背包即可。代码见下方:

POJ - 1742 Coins

  题目大意: 有n种硬币,这n种硬币的价值为a[i],第i种硬币的个数为c[i]个,问有多少种方案,使这些硬币加起来等于m。

  题解:这种题型可能有人说,叫背包方案数,但是其实就相当于是一个布尔型的多重背包问题,但是对于布尔型的多重背包问题,除了二进制拆分之外,有一种更为有效的方法,在每次更新方案时,记录某个硬币使用的次数,当次数大于给出的个数时就不进行方案更新。除此之外就是普通的多重背包了,代码见下:

HDU - 5445 Food Problem

  题目大意:有n种食物,m种车,每种食物都有提供的能量值,占的空间,和数量,每种车有价值,可用空间,和数量,求获得一定能量最少需要花费多少代价。

  题解:这是今年长春赛区网络赛的题目,乍一看,似乎是颇为复杂的题目,主要在于车和食物两边都有限制,没有一个固定的可供代价可以分配,所以显然一遍背包问题是不够的,那么做两遍背包问题呢?首先用背包计算出要获取一定数目的能量,最少需要占用多少空间,然后计算出获得题目所需能量,最少需要多少空间,同理,接下来,第二次使用背包计算出使用不同数目的租金,最大可以获得多少空间,那么大于空间的最小租金就是答案,同时多重背包可以用二进制拆分优化。

HDU - 3339 In Action

  题目大意: 有n个电站,每个电站都有一定的电量,电站之间有一定距离,我们要从0点出发去占领一些电站,使得占领的电站电量之和超过总电量的一半,求达到条件所要走的最短距离。如果可能的话,输出距离,否则输出不可能。

  题解:这道题涉及到了背包问题和其它算法的组合,个人感觉非常巧妙,首先计算所有点到起点的最短路,就得到了代价,然后又有价值,那么显然就是一个01背包问题,于是得解。

  暂且枚举这么几题我觉得比较典型的题目,加深一下对背包系列问题的理解。

   背包问题的解法在许多动态规划的题目中都有一定的参考意义,熟练掌握,能举一反三,对其它类型的动态规划也有一定的帮助,所以,认真学习一下背包问题还是很有必要的。

  向日葵的季节已经过去,ACM的路还长。

这篇关于背包问题就是陪你看花开向阳的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/956696

相关文章

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

SpringBoot启动报错的11个高频问题排查与解决终极指南

《SpringBoot启动报错的11个高频问题排查与解决终极指南》这篇文章主要为大家详细介绍了SpringBoot启动报错的11个高频问题的排查与解决,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一... 目录1. 依赖冲突:NoSuchMethodError 的终极解法2. Bean注入失败:No qu

MySQL新增字段后Java实体未更新的潜在问题与解决方案

《MySQL新增字段后Java实体未更新的潜在问题与解决方案》在Java+MySQL的开发中,我们通常使用ORM框架来映射数据库表与Java对象,但有时候,数据库表结构变更(如新增字段)后,开发人员可... 目录引言1. 问题背景:数据库与 Java 实体不同步1.1 常见场景1.2 示例代码2. 不同操作

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

如何解决Spring MVC中响应乱码问题

《如何解决SpringMVC中响应乱码问题》:本文主要介绍如何解决SpringMVC中响应乱码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC最新响应中乱码解决方式以前的解决办法这是比较通用的一种方法总结Spring MVC最新响应中乱码解

pip无法安装osgeo失败的问题解决

《pip无法安装osgeo失败的问题解决》本文主要介绍了pip无法安装osgeo失败的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 进入官方提供的扩展包下载网站寻找版本适配的whl文件注意:要选择cp(python版本)和你py

解决Java中基于GeoTools的Shapefile读取乱码的问题

《解决Java中基于GeoTools的Shapefile读取乱码的问题》本文主要讨论了在使用Java编程语言进行地理信息数据解析时遇到的Shapefile属性信息乱码问题,以及根据不同的编码设置进行属... 目录前言1、Shapefile属性字段编码的情况:一、Shp文件常见的字符集编码1、System编码

Spring MVC使用视图解析的问题解读

《SpringMVC使用视图解析的问题解读》:本文主要介绍SpringMVC使用视图解析的问题解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring MVC使用视图解析1. 会使用视图解析的情况2. 不会使用视图解析的情况总结Spring MVC使用视图

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

Java程序运行时出现乱码问题的排查与解决方法

《Java程序运行时出现乱码问题的排查与解决方法》本文主要介绍了Java程序运行时出现乱码问题的排查与解决方法,包括检查Java源文件编码、检查编译时的编码设置、检查运行时的编码设置、检查命令提示符的... 目录一、检查 Java 源文件编码二、检查编译时的编码设置三、检查运行时的编码设置四、检查命令提示符