无可救药的背尼玛和包尼玛
ACM刷了一年多,背包学了无数次,学一次忘一次我也是醉了。
特此来纪念一下傻逼的我学习背包的结果。。。
顺便教教可爱又迷人的女朋友。
===========================尼玛,背了个包====================================
【包】:
首先我们要来说一下这个包,背的这个包不是普通的包,因为普通的包不会这么恶心的(生无可恋)。。。
这个包听说是个双肩包,不过我喜欢单肩包,这可能就是我记不住这个包的原因吧。。
包有两个参数,一个是可装的总体积 V;还有一个就是可以获得的总重量 W(一般我们也把这个重量表示为价值);
同时呢,我们往里塞的东西也是有相应的两个参数 v[ i ] , w[ i ] ;
两个参数对应的就是它占用的体积 和 他带来的价值;
好了,背景介绍完毕!
< 0 - 1 背了个包 >************************
0-1 背包就是背包的精神内涵了(可怕.jpg)。
【基本描述】: 给n个物品,每个物品有v[ i ],w[ i ];每个物品只有1个,问!这个包最大能装多少的价值;
【掰扯掰扯】:傻逼一点的人肯定会想找性价比高的放,当然是不行的喽。反例自举。
然后就诞生了0-1来背这个包;
我们设 Dp[ i ] 表示:当背包的容量为 i 的时候,可以装入的最大的价值,(Dp[ i ]表示获得的最大价值);
然后对于每一件物品(假设当前考虑的物品是第 j 件),我们枚举一下这件物品 放 还是 不放 到这个包里;
①:放
因为我当前最大的容量是 i ,所以我放这个物品进去就要给它空出来它这么大的容量v[ j ],
而且,空出来的这段容量所带来的价值是确定的,
就是w[ j ],所以此时,我背包的总的价值就是 Dp[ i ] = Dp[ i - v[ j ] ] + w[ j ];
(假设Dp[ i - v[ i ] ]是已知的);
②:不放
不放就是不放,就没有什么好说的了,此时的最大的价值就是 Dp[ i ] 它原来的值了;
③:你到底放还是不放啊!
当然是找两种方式中较大的一种喽。。
原因很简单嘛,加入此时 i = V, 那么此时就是最后的结果了,我当然是要大的那个喽。
其实更理论一点的说法是,动态规划 思维就是:通过子问题最优解得到全局的最优解,
所以每一步我们都要得到最优解。
然后我们的算法过程就很简单了:
for(int j = 0; j < n; j++) { for(int i = v; i >= v[j]; i--) { Dp[i] = max( Dp[i] , Dp[i-v[j]] + w[j] ); } }
代码中有一些需要解释的是,第一层枚举我当前的物品 j ,第二层是从最大的容量开始考虑的,
为什么呢,其实很简单,因为每个物品只能用一次,
i - v[ i ] 是比 i 容量小的状态,如果我们从小往大的枚举的话,那么在 i - v[ j ] 的状态的时候,
如果我们选择了放 j 进去,就意味着j 被用掉了,
并且改变了Dp[ i - v[ j ] ] 的值,我们后来的枚举就不能保证只使用一次 j 了。
倒着枚举可以避免这种状况的出现;
[ 注 ]:
使用前要记得把Dp数组清空为 0 ;
[ 问题变化 ]:
①:问你恰好装满的最大价值。
解决这种问题的时候只要把Dp数组初始化改变一下,DP[ 0 ] = 0; 其余的Dp全部赋为 [ 负无穷 ]。
[ 负无穷 ] 表示该种容量的时候不合法,这样在更新的时候,不合法的背包始终都会是负数,
而合法的背包容量就是正数。
最后判断一下Dp[ V ] 是否是负数就知道是不是合法的结果了。
< 完全 背了个包 >************************
完全背包就是0-1背包的简化版了。
【基本描述】:给你n个物品,每个物品还是那两个参数,但是每一个都可以使用无限多次;
【随便扯扯】:唯一的不同就是枚举的顺序,这次是从前往后的了。至于为什么不懂自己想。
for(int j = 0; j < n; j++) { for(int i = v[j]; i <= V; i++) { Dp[i] = max( Dp[i] , Dp[i-v[j]] + w[j] ); } }
其它的东西都是一样的了,包括初始化和问题变种;
< 多重 背了个包 >************************
多重背包就是完全背包和0-1背包的合体版了(就是这么可怕)。
【基本描述】:给你n个物品,每个物品还是那两个参数,但是每一个都可以使用的次数是给定的;
【随便扯扯】:唯一的不同就是枚举的顺序,这次不是从前往后的了,也不是从后往前的,
而是根据可用的数量,选择枚举的顺序。
这个模板使用了二进制优化;不懂以后慢慢理解;
为什么不写了呢,因为女朋友刚刚跟我生气来着,浪费了好长时间呢,所以不写了!
这告诉我们,哄好女朋友才是提高效率的唯一途径,没有的就算了。
#include<cstdio> #include<algorithm> #include<cstring> using namespace std; int Dp[60005]; void ZeroOne( int cost ,int weight ,int sum ) { for( int i = sum ; i>= cost ; i-- ) Dp[i] = max( Dp[i], Dp[i - cost] + weight ); } void Complete( int cost ,int weight,int sum ) { for( int i = cost ; i<= sum ; i++ ) Dp[i] = max( Dp[i], Dp[i-cost] + weight ); } void Multi( int cost ,int weight, int count, int sum ) { if( cost * count >= sum ) Complete( cost , weight, sum ); else { int i=1; while( i < count ) { ZeroOne( i*cost , i*weight, sum ); count -= i ; i<<=1 ; } ZeroOne( cost* count , weight * count , sum); } }
【进阶问题】:现在问题变了,物品还是原来的物品,现在我要问你能不能把在这个包装满,
我不考虑物品的价值,只问你能不能装满。你说!能不能装满!
其实都一样,就是个初始化的问题。。。
不过今天做到BC一道题,题目要求取 K 个装满 V,这个。。还要思考一下行不行
(其实就是下面要讲的这个 二维费用背包问题);
< 二维费用 背了个包 >************************
二维费用背包就是背包的升级版了。
【基本描述】:给你n个物品,每个物品三个参数,包括两个费用参数,我们可以简单的理解为 va[ j ] 和 vb[ j ];
【随便扯扯】:都说了是二维的了,此时的Dp当然就变成二维数组了,Dp[ U ][ V ] 表示这个包两种费用的最大承受值;
那么对于一个物品,还是那样子,放还是不放,方放几个。。。也就是上面的几种DP方式;
不同的是,对于状态转移的时候,要枚举两层了;
void ZeroOne(int cost_u,int cost_v, int weight, int u, int v) { for(int i= u; i>=cost_u; i--) for(int j=v; j>=cost_v; j--) Dp[i][j] = max( Dp[i][j], Dp[i-cost_u][j-cost_v]+weight); } void Complete(int cost_u,int cost_v, int weight, int u, int v) { for(int i= cost_u; i<=u; i++) for(int j=cost_v; j<=v; j++) Dp[i][j] = max( Dp[i][j], Dp[i-cost_u][j-cost_v]+weight); } void Multi(int cost_u,int cost_v, int weight,int count,int u,int v) { if(cost_u * count >=u && cost_v * count >=v) Complete(cost_u, cost_v, weight, u, v); else { int i=1; while( i < count ) { ZeroOne(cost_u*i, cost_v*i ,weight*i, u, v); count -= i; i<<=1; } ZeroOne(cost_u*count, cost_v*count, weight*count, u, v); } }
明天继续.。。。。。。。。挂机ing
< 分组 背了个包 >************************
分组背包就是背包的升升级版了。
【基本描述】:给你n个物品,每个物品三个参数,包括一个体积参数,一个价值参数,还有一个就是这个物品所在的组;
但是都不重要,重要的是每一组里只能选一个物品出来用,或者不选(应该是每种物品只用一次);
【随便扯扯】:二维的,此时的Dp当然就变成二维数组了,Dp[ K ][ V ] 表示前K组中,最大体积为V的状态获得的最大价值;
首先这个Dp有三层循环。。。
第一层枚举组,当前是第几组K。
第二层枚举背包最大体积V。
第三层枚举这一组中的所有物品 i。
顺序是不能变的,因为对于当前的体积,枚举每一个物品取一个最优解,才能保证只会使用这其中的一个或0个
同时枚举容量的时候倒着枚举,保证了正确性。
for(int k=1;k<=K;k++) for(int j = V; j>=0; j--) for(int i=1;i<=n;i++) { if( j-v[i] < 0) continue; Dp[j] = max(Dp[j], Dp[j-v[i]]+weight[i]); }
< 依赖关系 背了个包 >************************
依赖背包就是背包的升升升级版了。
【基本描述】:给你n个物品,每个物品使用的时候满足某个物品必须使用的要求,
可以理解为这些物品构成一个森林的关系结构,某个物品在取的时候必须满足其父亲节点已经取过。
【随便扯扯】:这种问题的解法是在上一种解法的基础上改进过来的,也是一种分组的思维。有点强行分组的感觉。
这里分组是这样的,对于一棵子树,我们考虑的是取几个物品,也就是0-n个。每一种取法之间都是
互斥的,也就是说我们可以把所有的可能性都列出来,然后用分组背包来做。但是。。所有可能性
会非常的多。也就是不行哒!
然后我们又进一步优化这种思维,对于这棵子树,在所有的可能性中,其实有很多使可以舍弃掉的
因为我的背包一共就V那么大,不同的取法中,取出来的体积大小只有V种,所以,我们可以先对
相同V的取法中选取一个最优的取法,然后在这V个取法中再考虑分组背包去做
(就是一组V件 物品取1件)。
这种题目比较多变了,没有固定的模板,都是在前面的基础上,利用几种Dp方程组合出来的。
正是由于变化多端,所以这类的题目比较多,难度多是中等题,这种思维是很多动态规划的基础。
一般解法就是:
在考虑某个物品的时候,必须将其所有的子物品都考虑完,更新到Dp数组中,、
并且在合法的范围内,然后我们在来考虑这个整棵子树中的最优解(就是这一组中的最优解)
但是这并不是一种显示的分组做法,其更新的过程都隐藏在了DFS的过程中,一般不好看出来
是分组DP但是在思考的时候确实是用了分组背包的思维,这个还要自己理解一下。
hdoj1011 是一道这样的题,其实这是过渡到树形Dp的基础了。
< 泛化物品 背了个包 >************************
泛化背包就是背包的升升……级版了。
【基本描述】:泛化物品就是说,给你一些物品,但是每个物品的价值并不是固定的,根据你分配给这个物品的V
而改变的,也就是其价值是一个在体积范围内的函数。
其实其它的背包也可以看做是泛化的物品,0-1背包中我们可以看做是除了v[i ]其它价值都是0.。。。。
最接近的一种就是分组背包中的了,对于一棵子树,分配给他的容量不同,最大的价值有可能就不同;
【随便扯扯】:泛化的物品其实就是函数的复合,但不是普通的在x轴上的复合,是一种很好理解的抽象复合,
即对于一个给定的容量,如果只有两个物品,那么我可以枚举分给每个物品的容量,取一个最大的。
对每一种v都得到最大的就是将两个物品函数复合成了一个,就相当于替换掉了,然后就继续枚举其它
的物品,最后会复合到一和函数中,然后最大值就是在定义域里寻找了。