在讲可依赖背包之前先回顾下背包九讲的内容,现在做dp多了发现背包问题真的是dp的鼻祖,好多种问题都是基于背包的模型发展而来,就是不是基于背包的,把背包搞懂也有利于你自己学习dp,以此来想到其它状态转移方程。
首先,一开始是背包的基础构成--01背包。一般类型就是下面这种:
题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
基本的状态转移方程是:f[i][v] = max(f[i - 1][ v], f[i - 1][ v - c[i]] + value[i]);
表示我当前的最优价值是我没放这一件之前的价值和我放了这一件之后的价值比较而得来。
基本模型是这样的:
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= v; j++)
{
if(j >= c[i])
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + value[i]);//代表着我没放了这个这一个物品的价值,和我放了这个物品的价值来比较
}
else
{
dp[i][j] = dp[i - 1][j];//如果我小于当前的体积那么我就等于我前一个因为放不了
}
}
}
第一种可以想到的优化是j直接从c[i]开始,这样不用特判。
那么继续优化呢?
在时间上无法优化了,但是在空间上是可以优化的
依旧是俩层
for(int i = 1; i <= m; i++)
{
for(int j = v; j >= c[i]; j--)
{
dp[j] = max(dp[j], dp[j - c[i]] + value[i]);
}
}
为什么这样可以呢?个人理解的是因为我i是每次遍历都是从上一个物品放完之后我才开始到我当前,所以我直接开一个一维的dp数组就可以,将i隐藏这样直接能将二维的空间省成一维的空间这个一维的空间隐含的含义就是从i-1开始判断我放还是不放,那么这就需要注意一个问题了:
如果是二维的话我j从1--v没有事,但是我一维从1-v就要出事情,因为其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,而且顺序的话会不断更新前面的值,我们要尽量保证前面的值是不被影响的。所以这才是逆序的关键,还有就是递推的问题,我们要保证从i-1推广而来,所以顺序是不被采取的。这里的话多讲讲01背包,因为后面所有的问题都是围绕01背包转换而来,所以01背包是关键。
还有就是初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。
完全背包
一般题目类型:
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路
这个问题非常类似于01背包问题 ,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种,每个物品最多取v/c[i]件。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程。
根据01背包很容易就能想到状态转移方程:
f[i][v] = max(f[i - 1][v], f[i - 1][ v - k * c[i]] + k * value[i]);
这是一种最容易想到的方程但是我们能不能优化了呢?我们可以这样想
若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然,因为我明明花费高但是我的价值还小,那么我肯定不要这一种啊,生活在的常识。所以我把这种情况加入判断可以大大的优化程序的运行时间,但是最坏情况都成立那就没办法了。然后另一种优化是什么呢?如果我当前的v<c[i]显然我不放了,所以我可以在程序中执行一下判断.
然后更加高明的优化是啥呢,我把它转化成01背包来求解,因为每个最多放v/c[i]件,所以我就可以把多件拆成1件,然后运用01背包代码判断。那么更更加高明的优化是啥呢??
我们01背包是每个物品只放1件,那么我们思维转化下,在考虑"加选一件第i种物品的策略"时,需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环将i变成放第i种物品,后面依旧是可以容纳的体积,不断的更新状态,所以我们的二维状态转移方程就出来了:
f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]};//这个方程的意思是当前我的最佳价值,由我没放这种物品和放了这种物品的件数来比较。
所以顺序循环的时候不断更新值。
那么显而易见的我们可以转化成一维的背包求解:
for i=1..N
for v=0..V
f[v]=max{f[v],f[v-c[i]]+value[i]};
从上面来说,01背包是重中之重理解了01背包之后才能更好的理解各种其他背包。
多重背包
题目
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有num[i]+1种策略:取0件,取1件……取num[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则有状态转移方程:
f[i][v] = max(f[i - 1][v - k * c[i]] + k * w[i]);
我们前面讲完全背包的时候有一种思路,每一种物品最多放v/c[i]件,所以我们可以完全把那个思想拿过来,转化成01背包求解,因为每种物品有num[i]个,我们就分解成num[i]个01背包,然后运用01背包求解就可以
基本模板:
memset(dp, 0 ,sizeof(dp));
int n, m;
cin >> n >> m;
int k = 1;
for(int i = 1; i <= m; i++)
{
int a, b,c;
cin >> a >> b >> c;
while(c)
{
weight[k] = a;
value[k] = b;
k++;
c--;
}
}
for(int i = 1; i < k; i++)
{
for(int j = n; j >=weight[i]; j--)
{
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[n] << endl;
混合三种背包问题
问题
如果将01背包、完全背包、多重背包混合起来。也就是说,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
01背包与完全背包的混合
考虑到在01和完全中给出的伪代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度是O(VN)。伪代码如下:
for i=1..N
if 第i件物品是01背包
for v=V..0
f[v]=max{f[v],f[v-c[i]]+w[i]};
else if 第i件物品是完全背包
for v=0..V
f[v]=max{f[v],f[v-c[i]]+w[i]}
再加上多重背包
or i=1..N
if 第i件物品是01背包
ZeroOnePack(c[i],w[i])
else if 第i件物品是完全背包
CompletePack(c[i],w[i])
else if 第i件物品是多重背包
MultiplePack(c[i],w[i],n[i])
做的背包题多了,发现基本套路就是这样看你怎么转化,从一开始的啥也不会到现在的能解决一些复杂的背包问题,我觉得勤思考,勤总结是非常重要的。
二维费用的背包问题
问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
算法
费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:
f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。
剩下的代码完全跟前面三种背包一样看你怎么想了,这里就不给实现了,我敲的也比较累,容我偷个懒。
分组的背包问题
问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
到了我最想讲的背包问题了,前面铺垫了那么多久为了讲分组和依赖背包问题。
这种问题看你的选择策略:
是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}//这里的含义又多了一种你要考虑当前组和前面的组的最大价值。
然后转化成01背包求解就行!
开结构体还是数组存看你喜好!
使用一维数组的伪代码如下:
for 1.....k//组
for v=V..0//容量
for i = 1....n//当前组的物品数量
f[v]=max{f[v],f[v-c[i]]+w[i]}
有依赖的背包问题
简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
这种问题是由金明的预算演变而来。
仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略
所以我们要先对附件进行01背包,把每个附件的最优解求出来在对主件进行01背包。先看金明的预算。
金明的预算方案
时间限制: 1 Sec 内存限制: 125 MB
提交: 9 解决: 7
[提交] [状态] [讨论版] [命题人:外部导入] [Edit] [TestData]
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
主件 | 附件 |
电脑 | 打印机,扫描仪 |
书柜 | 图书 |
书桌 | 台灯,文具 |
工作椅 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0个、1个或2个附件。附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大.
设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,...,jk,则所求的总和为:
v[j1]*w[j1]+v[j2]*w[j2]+ …+v[jk]*w[jk]。(其中*为乘号)
请你帮助金明设计一个满足要求的购物单。
输入
每组输入数据的第1行,为两个正整数,用一个空格隔开:
N m(其中N(<32000)表示总钱数,m(<60)为希望购买物品的个数。)
从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数
v p q(其中v表示该物品的价格(v<10000),p表示该物品的重要度(1~5),q表示该物品是主件还是附件。如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号)
输出
每组输出只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。
样例输入
复制
1000 5 800 2 0 400 5 1 300 5 1 400 3 0 500 2 0
样例输出
复制
2200
来源/分类
这道题我就考虑它的状态,选不选主件,选主件和附件1,选主件和附件2选主件和附件1、2然后进行01背包就出来结果了,
做完这道题再看下面的那道题就有思路了,其实也很简单。五种状态方程列出来就很简单了
//放主机
dp[j] = max(dp[j], dp[j - zj_w[i]] + zj_c[i]);
//放附件1
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][1]] + zj_c[i] + fj_c[i][1]);
//放附件2
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][2]] + zj_c[i] + fj_c[i][2]);
//俩个附件都放
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][2] - fj_w[i][1]] + zj_c[i] + fj_c[i][2] + fj_c[i][1]);
# include <cstdio>
# include <iostream>
# include <cstring>
# include <algorithm>
using namespace std;
const int maxn = 32005;
int zj_w[maxn],zj_c[maxn];
int fj_w[maxn][3], fj_c[maxn][3];
int dp[maxn];
void fun()
{
memset(zj_w, 0, sizeof(zj_w));
memset(zj_c, 0, sizeof(zj_c));
memset(fj_w, 0, sizeof(fj_w));
memset(fj_c, 0, sizeof(fj_c));
memset(dp, 0, sizeof(dp));
}
int main(int argc, char*argv[])
{
int n, m;
while(cin >> n >> m)
{
fun();
for(int i = 1; i <= m; i++)
{
int v, p, q;
cin >> v >> p >> q;
if(!q)
{
zj_w[i] = v;
zj_c[i] = v * p;
}
else
{
fj_w[q][0]++;
fj_w[q][fj_w[q][0]] = v;//放附件1 还是2
fj_c[q][fj_w[q][0]] = v * p;
}
}
for(int i = 1; i <= m; i++)
{
for(int j = n; j >= zj_w[i]; j--)
{
//放主机
dp[j] = max(dp[j], dp[j - zj_w[i]] + zj_c[i]);
//放附件1
if(j >= zj_w[i] + fj_w[i][1])
{
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][1]] + zj_c[i] + fj_c[i][1]);
}
//放附件2
if(j >= zj_w[i] + fj_w[i][2])
{
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][2]] + zj_c[i] + fj_c[i][2]);
}
//俩个附件都放
if(j >= zj_w[i] + fj_w[i][2] + fj_w[i][1])
{
dp[j] = max(dp[j],dp[j - zj_w[i] - fj_w[i][2] - fj_w[i][1]] + zj_c[i] + fj_c[i][2] + fj_c[i][1]);
}
}
}
cout<< dp[n] << endl;
}
return 0;
}
撸啊撸
时间限制: 1 Sec 内存限制: 128 MB
提交: 15 解决: 5
[提交] [状态] [讨论版] [命题人:外部导入] [Edit] [TestData]
题目描述
HaiLin非常喜欢玩LOL,他在玩LOL时,选择了一位具有远程攻击技能的英雄。如图,他把英雄放在原点位置(0,0)不动,然后让英雄利用远程攻击技能杀敌。在英雄的视角看,有一些敌人是共线的,英雄如果想杀掉一个敌人的话,必须把挡在这个敌人前面的那些敌人提前干掉才行。假设所有的敌人都被英雄永久冻结不能动弹了,每个敌人有一定的价值,英雄杀死每个敌人也需要一定的时间。而现在,英雄仅仅剩下很少的时间用来杀敌,HaiLin想知道英雄如何在仅仅剩余的时间里使得杀掉的敌人价值综合最大?英雄在同一时间只能攻击一个敌人。
输入
多组输入数据,每组第一行包含两个整数N(0<N<=200)和T(0<=T<=40000),以一个空格分隔,分别代表敌人的数量实际英雄用于杀敌的剩余时间。接下来输入N行数据,表示N个敌人的信息,每一行包括四个整数x、y、t、v,各以一个空格分隔。x和y代表某个敌人所处的位置坐标,0<=|x|<=1000000000,0<y<=1000000000。t代表英雄杀死这个敌人需要花费的时间,v代表这个敌人的价值,0<t<=200,0<=v<=200,输入以文件尾结束。
输出
对于每组输入数据,输出英雄在给定剩余时间内能够杀死敌人的最大价值和。每组输出独占一行。
样例输入
复制
3 10 1 1 1 1 2 2 2 2 1 3 15 9 3 10 1 1 13 1 2 2 2 2 1 3 4 7
样例输出
复制
3 7
提示
先将他转化成可依赖问题,然后转化成01背包就可以需要注意的是,控制斜率相等的精度问题,还有就是0<=x<maxn,0<y<maxn
把控好这俩个就能a一个简单的裸背包问题
# include <iostream>
# include <cstring>
# include <string>
# include <algorithm>
# include <cmath>
using namespace std;
const int maxn = 40005;
struct node
{
double x, y;
double k;
int v, w;
}s[205];
int dp[maxn];
double cmp(struct node a, struct node b)
{
if(abs(a.k - b.k) <= 1e-30)
{
return a.y < b.y;
}
return a.k < b.k;
}
int main(int argc, char *argv[])
{
int n, t;
while(cin >> n >> t)
{
memset(dp, false, sizeof(dp));
for(int i = 1; i <= n; i++)
{
cin >> s[i].x >> s[i].y >> s[i].w >> s[i].v;
s[i].k = s[i].x / s[i].y;
}
sort(s + 1, s + 1 + n, cmp);
for(int i = 1; i <= n; i++)
{
if(abs(s[i].k - s[i- 1].k) <= 1e-30)
{
continue;
}
for(int j = t; j >= 0; j--)
{
int nw = 0, nv = 0;
for(int k = 0; k + i <= n && abs(s[i].k - s[i + k].k) <= 1e-30; k++)
{
nw += s[i + k].w, nv += s[i+k].v;
if(j >= nw)
{
dp[j] = max(dp[j], dp[j - nw] + nv);
}
}
}
}
cout << dp[t] << endl;
}
return 0;
}