算法--动态规划(背包问题)

总览

在这里插入图片描述

dp问题的优化

在这里插入图片描述
要清楚:dp问题的优化一般是对dp问题的代码或者计算方程做一个等效变形
有了这个前提,我们在写dp问题时,要先将基本的代码写出来,之后再做优化

01背包问题

概述

在这里插入图片描述
假设我们有N个物品,我们的背包的体积是V,
N个物品每个物品有两个属性,分别是v体积、和w价值,或者说权重,每个物品要么不选,如果选的话,只能选一次
我们的目标是:要选出一些物品,在总体积能装的下的情况下(不一定必须装满),争取价值之和最大化

算法思想

在这里插入图片描述
Dp问题,要考虑两个问题,一个是状态表示,一个是状态计算
对于01背包问题,
状态表示:
我们先来看状态表示,因为大前提是N个物品和V的体积,也就是不算物品属性的话,我们有两个参数,所以,状态表示f,就有两个参数,那他就是二维的状态表示,f(i,j)
而对于一个状态表示 f,我们要清楚,他是一个集合,那么一个集合就会有属性,一个集合有三种属性,根据不同的题设,选择不同的属性,三种属性分别是max(元素最大值)、min(元素最小值)、数量(元素数量),本题根据题设,是属性选定为max,表示求最大价值
那这个集合的元素表示什么呢,该集合的元素表示在所有的选法中,只从前 i 个物品选择,总体积小于等于 j 的选法
总结:状态表示就是将题意数学化,将题设信息数学化为 f(i,j)
状态计算:
状态计算就是对上面的 f(i,j)进行计算,主要用到集合划分的思想
首先将集合划分为两部分,
第一个部分是 f 集合中所有不含 i 的选法的最大值,那么就是从1~i-1中选,总体积不超过 j ,也及时 f(i-1,j)
第二个部分是 f 集合中所有含 i 的选法的最大值,那么我们先将 i 排除,求得不算 i 时剩余的值最大的选法,因为排除了一个 i ,那么体积限制也跟着缩小,所以是从1~i-1中选,总体积不超过 j-vi 的选法的元素的最大值,即 f (i-1,j-vi)

最后,将两部分取maxAPI,求得 f(i,j),注意,因为第二部分是将 i 排除之后计算得出的,所以,在取max时,第二个参数要加上i的价值wi,即第二个参数为 f(i-1,j-vi)+ wi
如下三张图
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

算法思想中的注意点

首先,f(i,j)表示在前i个物品中选,总体积不超过 j 的选法,最大价值的值
这里的i 和 j 是通量,相当于将题设扩展成普遍性了,不是题设的最终量,也就是 i 和 j在代码中是变化的,最后是要输出f(N,V)

其次,对于状态计算,不含i的部分一定会出现,含i部分未必一定会出现,所以代码中要加一个判断,代码中会体现,注意留意

再者,代码的循环存在从0到等于终点,而不是0到小于终点或者1到等于终点,这点要注意,只不过有些循环的0初始值就是0,所以循环从1开始到等于终点,具体循环的范围,要根据代码原理、功能具体分析,不要想当然,要实事求是

例题+代码

在这里插入图片描述
在这里插入图片描述
n,m分别表示有n个物品,总体积是m
v[N],w[N]数组,分别存储每个物品的体积和价值
f[N][N],是状态表示,也就是前i个物品中选,总体积不超过j的选法中,价值最大的值

main函数里,
首先输入n和m
然后for循环,依次向v,和w中输入值(注意,i从1开始到等于n,因为这里是赋值下标物品的数据,没有第0个物品,所以i从1开始到等于n)
之后,双重循环 i 从1到等于n,j从0到等于m(根据分析,这里的双重循环是给f[][]的下标赋值的,而f[][]的下标表示从前几个物品里选总体积不超过j,而不是第几个物品以及某个物品的体积,所以,应该从0开始到等于n、m,至于为什么i从1开始而不从0开始,是因为f[0][0~m],表示前0个物品中,选出体积不超过 j的选法,因为物品是0,所以总价值也是0,所以f[0][0到m]都是0,又因为int定义自动初始化为0,所以不用管 i =0的情况)
循环内,f[i][j] = f[i-1][j],先将这个赋值给f[i][j]
之后判断第二部分,因为第二部分只有在 j>=v[i]时,才会出现,所以加上if判断之后,f[i][j] = max( f[i][j] , f[i-1][ j-v[i] ] + w[i] )
(因为第一步直接将值赋给了f[i][j],所以这里是用f[i][j]进行对比,这样做的好处是,将第一部分与第二部分隔离开,因为第二部分需要特判)

等效为一维数组

在这里插入图片描述
首先我们将 i 维直接去掉,接下来代码中的 i 维也跟着去掉(因为该语句不在循环里,所以不用考虑在哪一层,直接去掉 i 维即可,而在循环的话,要考虑先后,因为一维的话使用的是递归原理,先后很重要,对应到代码就是下标大小和循环顺序,见下面分析),这样的话,双重循环中的第一行代码就变成了上图所示,由于f[ j ] = f[ j ] 是恒等式,所以可以直接去掉
在这里插入图片描述
之后,由于此时仅剩第二条代码,所以可以对 j 的循环范围进行更改,将if去掉,因为只有 j >= v[i] 才会执行第二条语句,而 j 又小于等于m,也就是对于每个i,j的范围都是v[ i ] ~ m,所以,直接将j的循环范围改成 j=v[i] ; j <= m ; j++,然后将if去掉
在这里插入图片描述
然后直接将这行代码的 i 维去掉,就变成了上图所示,但是直接去掉是不合法的,应该进行一些其他变动
原理:(选择性查看)但是这样的话,从一维数组考虑,由于f[ j - v[i] ] 中的 j - v[i] 肯定小于 f[ j ] 中的 j ,而 j 是从小到大递增的,所以可以知道,f[ j - v[i] ] 在 f[ j ]被计算之前就被计算了,所以当前他已经有结果了,可以放在当前层,所以等效为上图箭头所示,而我们原来的代码中,第二个参数应该是f[i-1][j-v[i]+w[i]],所以,直接去掉是不合法的,应该进行一些其他变动
在这里插入图片描述
我们可以将j的范围掉头,让其从大到小进行遍历,就合法了
原理:(选择性查看)这样的话,从一维的角度来看,f[ j - v[i]] 在 f[j] 被计算之后才会被进行计算,也就不会在i层,而是等待被计算,又因为j-v[i]小,i从小到大遍历,所以这时等效为上图所示,f[i-1] [j - v[i] ] ,这时,就完成了等效
在这里插入图片描述
最终的代码等效为了上图,这就是01背包代码的终极形态

完全背包问题

概述

在这里插入图片描述
完全背包问题,是每个物品有无限个,每个物品都可以选无限次

算法思想

在这里插入图片描述
仍然是那几步,只不过这次前 i 个物品可以用无限次
主要区别在于状态计算,这里的划分依据是,对于第i个物品,选了几次
如果是选了0次,那么就是 f[ i-1 , j ]
如果选了k次,那么还是根据曲线救国的思想,先将k个i去掉,剩余部分求最大值,最后加回来k个i的价值
即 f[i-1 , j-kv[i] ] + kw[i]
(之所以还是 i -1 ,是因为 f 的含义是前i个物品,总体积不超过 j ,并没有强调前i个物品的次数,现在只是去掉了一种物品: i ,并没有涉及到 i 的次数)

例题+代码

在这里插入图片描述
在这里插入图片描述
这里又加了第三重循环,是加入k,表示物品 i 用了k次,因为又总体积和物品 i 的单位体积存在,所以k是有限的,k初始化为0,条件为 k * v[i] <= j
之后代码中直接用f[i][j] , 和 f[i - 1][j - k *v[i]] + k *w[i],进行max,这里之所以没有01背包中的两行代码是因为这里的划分,统一用的是一个公式,仅一个公式就包括了所有的划分情况(即 f[i - 1][j - k *v[i]] + k *w[i]这个公式),所以,直接一行代码即可,没有其他特殊情况

等效为二维数组

在这里插入图片描述
根据公式,当k从0开始取不同值的时候,我们可以得到第一行式子
假如我们求一下 f [i,j-v] ,k也从0开始取不同值,我们可以得到第二行式子,发现第二行式子加上w,就是第一行式子中,k!=0的结果,所以可以根据这个关系,将k的for循环删去,等效为双重循环
所以,等效之后的代码与01背包的朴素算法长的极为相似,唯一不同的是,在if后面的代码,这里是 f[i][j-v[i]]+w[i],而前面是f[i-1][j-v[i]]+w[i]
在这里插入图片描述
上图修改:注意最后加上w[ i ]
在这里插入图片描述

等效为一维数组

在这里插入图片描述
由于上面已经优化成二维数组了,且和01背包的代码仅仅差别在max的第二个参数中 f 数组的第一个参数,完全背包是 f[i][j-v[i]]-w[i],而01背包是 f[i-1][j-v[i]]+w[i]
完全背包的代码正好符合01背包向一维数组转换时,等效完 for循环中 j 的范围之后的代码,也就是完全背包问题,在转为一维时,还是跟01极为相似,不同的是,完全背包问题不用将 j 改为从大向小递减,其他均一致
(因为这里完全背包的代码,循环语句中恰好第一维都是[ i ],所以,直接去掉,无需更改循环变量的循环顺序)

多重背包问题

概述

在这里插入图片描述
多重背包问题,是每个物品的个数不一样,也就是每个物品的可选次数不一样

算法思想

在这里插入图片描述
状态表示仍然是那个,不同的是状态计算
而状态计算与上一个:完全背包问题极为相似,上面完全背包问题是每种物品都有无限次可以选,唯一的限制是体积不能超过总体积,
而这个多重背包问题,因为每组可选的次数为s[i],不再是无限次,所以,这里的状态计算是:第i个物品选了k次,k从0到s[i]
所以,代码唯一与完全背包问题不同的是,k的限制条件+1:k <= s[i];且多了一个s[ ]数组,在输入的时候,将物品i能选几次输入到数组s[i]中

例题+代码

在这里插入图片描述
在这里插入图片描述

优化(采用二进制的方式)

基本思想

在这里插入图片描述
原来的代码中,对于每个物品i,k都是从0开始递增循环,现在我们可以优化这个k的循环次数,让他不再一次一次循环
假如说,目前物品 i 的s[i] 是200,那么按照常规理解,k就要从0一直循环到200,但是,根据进制数的启发,对于一个数(例如200),他可以表示成十进制,也可以表示成二进制,如上图所示,0~127,都可以用2的0次方 到 2的6次方 加起来得到,因为一个二进制转化为十进制就是这样转化的:由几个二进制的基础数加起来得到(且每一项只能用0或者1次,所以,对于当前例子来说,64能到达的极限是127,而128能到达的极限已经大于200了,所以不能取到128),但是因为642=128,1282-1已经超过了200,所以128不能使用,最后差多少就补上这个差值,这样,我们就可以用几个数表示出来0到200的所有数了

而有了这个思想,我们可以将这些次数与当前物品的v和w进行结合,也就是将其打包成一个大物品,一种基础物品有s次,可以被打包成好几个大物品,将所有的物品进行打包完之后,就可以转化成01背包问题了,这样即符合v和w,而且次数也对应可以凑出来,直接拿着打包好的这些物品,进行01背包的算法即可

时间复杂度

在这里插入图片描述
通过这样的优化,时间复杂度由NVS,优化到了NVlogS,其中logS是以2为底的对数的缩写
这时候带入数据量,如果算出来数据量最终结果为1e7~1e8,那么就可以在1s内算出来

例题+代码

在这里插入图片描述
使用该二进制进行数据打包时,特别注意的是,N开到了25000(如下图),而原始数据是1000,因为这里的1000仅仅是小物品的种类数,而每个物品有S个,但是我们进行打包时,一个物品 有S个 ,一个物品可以打包成logS个,所以就是1000logS,带入S=2000,再加上空余量,大概就是25000
在这里插入图片描述
这里都是在打包物品,(这里将视野放在一种物品的打包即可,其他的交给for循环)
首先输入n和m
之后定义一个cnt=0,用来给打包好的物品编号(后续称为大物品)
for循环从1到等于n
定义 a b s 用来接收每个基础物品(后续称为小物品)的体积价值和次数
之后 定义k=1(用来记录2的进制数)
while循环,k <= s(为什么是k <= s,我们可以看到第27、28行,首先对于当前这个小物品,每次while循环都会将s进行更新,也就是每次s表示的都是减去目前最大的二进制之后,剩余的次数,而k表示的是下一次将要生成的2的进制数(因为每个循环步的最后都有一个k
= 2)而如果剩余的次数小于下一次将要生成的2的进制数,那么就说明下一次已经越界了,不可用了,不用再进入循环了,所以,就有k <= s )
之后cnt++;(编号++)
v[cnt] = a * k
w[cnt] = b * k
这里就是将s个小物品打包成大物品的过程
之后s -= k
k *= 2
while循环结束后,有一个if判断,判断当s > 0 时,表示有剩余,最后将剩余次数进行打包即可
cnt++
v[cnt] = a * s;
w[cnt] = b * s;
在for循环结束之后,也就是所有的物品打包完之后,
最后将n更新为cnt,表示目前有cnt个大物品
在这里插入图片描述
最后将这些大物品进行一遍01背包的代码即可

分组背包问题

概述

在这里插入图片描述
分组背包问题,是会将物品进行分组,每一组最多只能选择改组内的一个物品,要么这个组不选,如果想选择这个组里的物品,只能选择改组内的一个物品且一次

算法思想

在这里插入图片描述
注意 这里的状态表示,从前i个变成了前i组,
状态计算,划分的时候,划分依据是,选择第i组的第k个,k可以为0(表示不选第 i 组)
如果k=0,那么就是第 i 组不选,那么就是f[i-1 , j ]
其他的 如果选第 i 组的第 k 个,那么就是 f [ i-1 , j - v[i, k]] + w[i,k]
与01背包差不多,只不过这里的 i 表示组,组中一旦有某个物品选了,那么该组就不可以第二次被选了,v、w用二维数组精确到组内成员

代码+例题

在这里插入图片描述
在这里插入图片描述
首先,s[N]是用来存储每一组有多少个种类的
输入n、m
之后for循环i从1到等于n,输入s[i]
且二重循环for,j从0到小于s[i],输入每个物品的v、w,因为i表示组,所以是w[i][j]、v[i][j]
(注意,因为这里对v和w赋值时用到的 j 是从0开始到小于s[i],那么之后再使用v和w时,那个循环变量也要从0到小于s[i])
其次,开始01背包一维代码的套用,
首先一个for,i 从1到小于等于n
之后for,j从大到小,j从m到大于等于0,j–(其实这里的j>=0,本来对应的是 j >= v[i][k],但是k在第三重循环,所以这里改为0,将条件加到三重循环里面)
在之后三重循环for,k从0到小于s[i](这里之所以不像之前多重背包那样有第二个限制条件:k * v[ i ] <= j,是因为这里的k个物品,每个物品之间都是不一样的,所以直接从0到小于s [ i ],即可)
if ( j >= v[i][k]) f[ j ] = max (f[j] , f[j-v[i][k]]+w[i][k]);
最后输出 f[m]即可

总结来说,他就是01背包和多重背包的结合。思路上,将物品差异化到每个组内的成员。代码上,要有跟多重背包一样的k循环,且与输入时相对应,实际上这里与多重循环不同的是,这里表示第k个物品,而多重循环表示有k个物品

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值