01背包

来源:互联网 发布:刻绘大师大软件下载 编辑:程序博客网 时间:2024/06/05 01:04
01背包
问题的引入: 01背包简化版
        现有N个物品,每个物品重量为W,这些物品能否使载重量为S的背包装满(即重量和正好为S)?如果不能正好装满,那么背包最多能装多少重量的物品?
【输入文件】
         第一行两个正整数N(0 < N < 1000)和S(0 < S < 10000)表示物品的个数和背包的容量,第二行N个整数列出这N个物品各自的重量。
【输出文件】
         单独一行,表示背包最大的载重量。
【输入样例】
         6 24
         8 3 12 7 9 7
【输出样例】
         24

【分析】
         针对这一问题我们以物品的个数分阶段,设计一个状态dp[i]表示载重量为i的背包可否装满,显然dp[i]的基类型是bool。 
        决策是什么呢?
         当要装第i个物品时,如果前i-1个物品可以使载重为 k的背包装满,那么载重为k+w[i]的背包就可以装满。于是对于一个dp[j]来说,只要dp[j-w[i]]是true(表示可装满)那dp[j]就可以装满。
         但要注意:针对每一个物品枚举背包的载重量时如果这样正向的推导会使同一个物品用好几次,因为k+w[i]可装满那么k+w[i]+w[i]就可装满,但实际上是装不满的,因为物品只有一个。解决这个问题很简单,只要逆向推导就OK了。
         这样划分阶段,设计状态,满足无后效性和么?
         显然对于每一个物品(阶段)都是独立的,物品的顺序并不影响求解,因为装物品的次序不限。而对于dp[j]只考虑dp[j-w[i]]而不考虑后面的,所以满足无后效性。
         有了上面的分析不难写出状态转移方程:
        
dp[j] = dp[j-w[i]]       {dp[j-w[i]] = true}
参考程序:#include <iostream>#include <cstdio>using namespace std;int w[1005], dp[1005];int main(){    int n, m;    scanf("%d%d", &n, &m);    for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);    dp[0]=1;    for (int i = 1; i <= n; i ++)         for (int j = m; j >= w[i]; j --)if (dp[j-w[i]]==1) dp[j] = 1;    for (int i = m; i >=0; i --)        if (dp[i] == 1){ printf("%d\n", i); break; }    return 0;    }
【例题1】 装箱问题 
        有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30=,每个物品有一个体积(正整数)。
         要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
【输入文件】
         第一行一个正整数V表示箱子的容量,第二行一个正整数N表示物品个数,接下来N行列出这N个物品各自的体积。
【输出文件】
         单独一行,表示箱子最小的剩余空间。
【输入样例】
         24
         6
         8
         3
         12
         7
         9
         7
【输出样例】
         0

【问题分析】
         本题是经典的0/1背包问题,并且是0/1背包的简化版,把箱子看做背包,容量看做载重量,体积看做重量,剩余空间最小也就是尽量装满背包。于是这个问题便成了:
         有一个栽重量为V的背包,有N个物品,尽量多装物品,使背包尽量的重。
         设计一个状态opt[i]表示重量i可否构成。
         状态转移方程:opt[j]:=opt[j-w[1]] {opt[j-w[i]]=true}
         最终的解就是v-x(x<=n 且opt[x]=true 且 opt[x+1..n]=false)。
参考程序:#include <iostream>#include <cstdio>using namespace std;int w[1005], dp[1005];int main(){    int n, m;    scanf("%d%d", &n, &m);    for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);    dp[0]=1;    for (int i = 1; i <= n; i ++)         for (int j = m; j >= w[i]; j --)    if (dp[j-w[i]]==1) dp[j] = 1;    for (int i = m; i >=0; i --)        if (dp[i] == 1){ printf("%d\n", m-i); break; }    return 0;    }
【例题2】 砝码称重
        设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000),用他们能称出的重量的种类数。
【输入文件】
         a1 a2 a3 a4 a5 a6
         (表示1g砝码有a1个,2g砝码有a2个,…,20g砝码有a6个,中间有空格)。
【输出文件】
         N
         (N表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)。
【输入样例】
         1 1 0 0 0 0
【输出样例】
         3

【问题分析】
         把问题稍做一个改动,已知a1+a2+a3+a4+a5+a6个砝码的重量w[i], w[i]∈{ 1,2,3,5,10,20} 其中砝码重量可以相等,求用这些砝码可称出的不同重量的个数。
         这样一改就是经典的0/1背包问题的简化版了,求解方法完全和上面说的一样,这里就不多说了,只是要注意这个题目不是求最大载重量,是统计所有的可称出的重量的个数。
参考程序:#include <iostream>#include <cstdio>using namespace std;const int a[6]={1,2,3,5,10,20};int w[1005], dp[1005];int main(){    int ans=0;    for (int i = 0; i < 6; i ++) scanf("%d", &w[i]);    dp[0]=1;    for (int i = 0; i < 6; i ++)         for (int j = 1; j <= w[i]; j ++)           for(int k = 1000; k > 0; k --)if (dp[k-a[i]]==1) dp[k] = 1;    for (int i = 1000; i >0; i --)        if (dp[i] == 1) ans++;printf("%d\n", ans);     return 0;    }
【例题3】 积木城堡
        XC的儿子小XC最喜欢玩的游戏用积木垒漂亮的城堡。城堡是用一些立方体的积木垒成的,城堡的每一层是一块积木。小XC是一个比他爸爸XC还聪明的孩子,他发现垒城堡的时候,如果下面的积木比上面的积木大,那么城堡便不容易倒。所以他在垒城堡的时候总是遵循这样的规则。
         小XC想把自己垒的城堡送给幼儿园里漂亮的女孩子们,这样可以增加他的好感度。为了公平起见,他决定把送给每个女孩子一样高的城堡,这样可以避免女孩子们为了获得更漂亮的城堡而引起争执。可是他发现自己在垒城堡的时候并没有预先考虑到这一点。所以他现在要改造城堡。由于他没有多余的积木了,他灵机一动,想出了一个巧妙的改造方案。他决定从每一个城堡中挪去一些积木,使得最终每座城堡都一样高。为了使他的城堡更雄伟,他觉得应该使最后的城堡都尽可能的高。
任务:
         请你帮助小XC编一个程序,根据他垒的所有城堡的信息,决定应该移去哪些积木才能获得最佳的效果。 
【输入文件】
         第一行是一个整数N(N<=100),表示一共有几座城堡。以下N行每行是一系列非负整数,用一个空格分隔,按从下往上的顺序依次给出一座城堡中所有积木的棱长。用-1结束。一座城堡中的积木不超过100块,每块积木的棱长不超过100。 【输出文件】
         一个整数,表示最后城堡的最大可能的高度。如果找不到合适的方案,则输出0。
【输入样例】
         2
         2 1 –1
         3 2 1 -1 
【输出样例】
         3

【问题分析】
         首先要说明一点,可以挪走任意一个积木,不见得是最上面的。 初看题目有点茫然,但抽象一下就。。。。。。。。。。
         其实堆好积木再拿走就相当于当初堆积木的时候没选拿走的积木。
         这样一转化思维问题就清楚了。把积木可搭建的最大高度看做背包的载重,每块积木的高度就是物品的重量。也就是用给定的物品装指定的包,使每个包装的物品一样多,且在符合条件的前提下尽量多。
         这样就变成经典的背包问题了。
         对于每一个城堡求一次,最终找到每一个城堡都可达到的最大高度即可。
【参考程序1】使用二维数组(可优化到一维)使用二维数组dp[i][k]表示第i个城堡是否可以堆到k的高度#include <iostream>#include <cstdio>#include <cstring>using namespace std;bool dp[102][10002];int a[101], maxx, t;int main(){    int x,n;    memset(dp,true,sizeof(dp));    scanf("%d", &n);    for(int i=1;i<=n;i++){t=0, maxx=0;scanf("%d", &x);while(x!=-1){a[++t]=x, maxx+=x;scanf("%d", &x);}dp[i][0]=false;for(int j=1;j<=t;j++)  for(int k=maxx;k>=a[j];k--)    if(dp[i][k-a[j]]==false) dp[i][k]=false;    }    for(int k=maxx;k>0;k--){t=0;for(int i=1;i<=n;i++)if(dp[i][k]==true){ t=1;break;}if(t==0){ printf("%d\n", k); break;}    }if(t==1) printf("%d\n", 0);     return 0;    }【参考程序2】使用一维数组使用dp[k]表示第i个城堡是否可达到k高度,如果能够达到,b[k]就增加1。一维数组b[k]表示有多少个城堡堆到k的高度,最后从大到小寻找第一个达到n的即为本题的解。#include <iostream>#include <cstdio>#include <cstring>using namespace std;bool dp[10002];int a[101], b[10002], maxx, t;int main(){    int x,n;    memset(dp,true,sizeof(dp));    scanf("%d", &n);    for(int i=1;i<=n;i++){t=0, maxx=0;scanf("%d", &x);while(x!=-1){a[++t]=x, maxx+=x;scanf("%d", &x);}memset(dp,true,sizeof(dp));dp[0]=false;for(int j=1;j<=t;j++)  for(int k=maxx;k>=a[j];k--)    if(dp[k-a[j]]==false) dp[k]=false;for(int j=maxx;j>0;j--)  if(dp[j]==false) b[j]++;     }    t=0;    for(int k=maxx;k>0;k--){if(b[k]==n){ printf("%d\n", k); t=1;break;}    }if(t==0) printf("%d\n", 0);     return 0;    }
         回顾上面的内容充分说明动态规划的本质就是递推。其实按照我的理解(动规涉及最优决策,递推是单纯的总结)背包问题的简化版更准确点算是递推而非动态规划,至于动归和递推之间的界线本来就很模糊(至少我这么认为)把它看做什么都可以,没必要咬文嚼字。
         回到0/1背包的原问题上(如果你忘了就去上面看看)。
         如果在不知道这个模型的情况下我们怎么做这个题呢?
         这就要用到第一节提到的方法二:三要素法。
         题目中明确说明对于一个物品要不拿走要不放下,其实题目赤裸裸的告诉我们决策就是不拿(用0表示)或拿(用1表示)。这样想都不用想就知道了决策,这也是本题的突破口。知道了决策写个搜索的程序应该是很轻松的了。
         那么阶段是什么呢?
         显然,给你一堆东西让你往包里塞,你当然是一个一个的那来,塞进去。那么阶段很明显就是物品的个数。
         状态又是什么呢?
         有的人在装东西是有个习惯(比如说我)就是先把东西分类,然后把同类的东西打个小包,最后在把小包放进去,我们可以按这个思想给物品打一些小包,也就是按照单位为1的递增的顺序准备好多小包,比如载重是6的包,可以为它准备载重是1,2,3,4,5的小包。这样状态就可以想出来了: 
        设计状态dp[i][j]表示装第i个物品时载重为j的包可以装到的最大价值。
         dp[i-1][j] (j-w[i]<0,i>0)
         状态转移方程:
         dp[i][j] = max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]}    (j-w[i]>=0,i>0)
         (w[i]:第i个物品的重量,v[i]第i个物品的价值)

解释:要载重为j的背包空出w[i](j-w[i])的空间且装上第i个物品,比不装获得的价值大就装上它。
         边界条件:dp[0][i] = 0; (i∈{1..S})
         注:
         这种二维的状态表示应该在下节讲,但是为了方便理解先在这里说了。
         上面的方法动态规划三要素都很明显,实现也很简单。但是在我初学背包时却用另外一种一维的状态表示法。
         用第一节说的思考方法五(放宽约束和增加约束)在重新思考一下这个问题:
         怎么放宽约束呢?
         把题目中的价值去掉,不考虑价值即最优就变成背包问题的简化版了。那简化版的求解对我们有何启示呢?
         再一次增加约束:背包只能装满。
         显然对于N个装满背包的方案中只要找到一个价值最大的就是问题的解。那么装不满怎么办呢?其实装不满背包,它总要达到一定的重量(X)。我们可以把这种情况看作是装满一个载重为X的小包。
         总结一下上面的思维过程:
         放宽约束让我们找到问题的突破口——和背包问题简化版一样,我们可以确定载重为S的背包是否可以装满。
         增加约束让我们找到问题的求解方法——在装满背包的方案中选择最优的一个方案。
         这样问题就解决了。
         设计一个状态dp[j]表示装满载重为j的背包可获得的最大价值。对于第i个物品,只要dp[j-w[i]]可以装满且dp[j-w[i]]+v[i]比dp[j]大就装上这个物品(更新dp[j])。
         怎么使dp[j]既有是否构成又有最优的概念呢?
         dp[j]只表示最优,只不过使初始条件+1,判断dp[j]是否为0,如果dp[j]=0说明j装不满。
         边界条件:dp[0]=1;
         状态转移方程:dp[j] = max{dp[j-w[i]]}      (0<i<n,w[i]<=j<=S)
         问题解:ans = max{dp[i]}-1      (0<i<=s)
         下面看几个例题:
【例题4】 采药
        辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
         如果你是辰辰,你能完成这个任务吗? 
【输入文件】
         输入文件medic.in的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
【输出文件】
         输出文件medic.out包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
【输入样例】
         70 3
         71 100
         69 1
         1 2 
【输出样例】
         3

【问题分析】
         这是一道典型的0/1背包问题,把时间看做标准模型中的重量,把规定的时间看做载重为T的背包,这样问题和基本模型就一样了,具体实现这里不多说了。
参考程序1:{二维状态}#include <iostream>#include <cstdio>using namespace std;int T, m, dp[1001][1001], t[1001], v[1001];int main() {cin >> T >> m;for(int i=1; i<=m; i++) scanf("%d%d",&t[i],&v[i]);for(int i=1; i<=m; i++)for (int j=1; j<=T; j++)if (j>=t[i]) dp[i][j]=max(dp[i-1][j], dp[i-1][j-t[i]]+v[i]); printf("%d\n",dp[m][T]);}参考程序2:{优化成一维}#include <iostream>#include <cstdio>#include using namespace std;int T, m, dp[1001], t[1001], v[1001];int main() {cin >> T >> m;for(int i=1; i<=m; i++) scanf("%d%d",&t[i],&v[i]);for(int i=1; i<=m; i++)for (int j=T; j>=t[i]; j--) dp[j]=max(dp[j], dp[j-t[i]]+v[i]); printf("%d\n",dp[T]);}
【例题5】 开心的金明
        金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的N 元。于是,他把每件物品规定了一个重要度,分为5 等:用整数1~5 表示,第5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过N 元(可以等于N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。设第j 件物品的价格为v[j],重要度为w[j],共选中了k 件物品,编号依次为j1...jk,则所求的总和为:
         v[j1]*w[j1] + v[j2]*w[j2] + … + v[jk]*w[jk] (其中*为乘号)
         请你帮助金明设计一个满足要求的购物单. 
【输入文件】
         输入的第1 行,为两个正整数,用一个空格隔开: N m
         (其中N(<30000)表示总钱数,m(<25)为希望购买物品的个数。)
         从第2 行到第m+1 行,第j 行给出了编号为j-1的物品的基本数据,每行有2 个非负整数 v p
         (其中v 表示该物品的价格(v≤10000),p 表示该物品的重要度(1~5)) 
【输出文件】
         输出只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<100000000)
【输入样例】
         1000 5
         800 2
         400 5
         300 5
         400 3
         200 2 
【输出样例】
         3900

【问题分析】
         这仍然是一到典型的0/1背包,只不过注意这个问题中的价值对应背包模型中的重量,这个问题中的价值和重要度的乘积是背包模型中的价值。(很饶口啊)。
参考程序:#include <iostream>#include <cstdio>#include  <algorithm>using namespace std;int n, m, dp[1001], v[30], w[30];int main() {int x,y;cin >> m >> n;for(int i=1; i<=n; i++){scanf("%d%d",&x,&y);w[i]=x; v[i]=x*y;  //v[i]记录价值和重要度的乘积,相当于01背包中价值} for(int i=1; i<=n; i++)for (int j=m; j>w[i]; j--) dp[j]=max(dp[j], dp[j-w[i]]+v[i]); printf("%d\n",dp[m]);}
【例题6】 金明的预算方案 
        金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过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]。(其中*为乘号)
         请你帮助金明设计一个满足要求的购物单。
【输入文件】
         输入文件budget.in 的第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是所属主件的编号) 
【输出文件】
         输出文件budget.out只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。
【输入样例】
         1000 5
         800 2 0
         400 5 1
         300 5 1
         400 3 0
         500 2 0 
【输出样例】
         2200

【问题分析】
         这道题是一道典型的背包问题,比较麻烦的就是它还存在附件和主件之分。但这并不影响解题,由于附件最多两个,那么我们做一个对主,附件做个捆绑就行了。
(1) 标准算法
         因为这道题是典型的背包问题,显然标准算法就是动态规划。由于我们要对主,附件做捆绑。由于题目没有直接给出每个主件对应的附件,所以还需要做一个预处理:另开两个数组q1,q2来分别记录对应的第i个主件的附件。那么对与附件不需要处理。而主件的花费就有4种情况了。( 下面用W表示花费)
         W1=v[i] (只买主件)
         W2=v[i]+v[q1[i]] (买主件和第一个附件)
         W3=v[i]+v[q2[i]] (买主件和第二个附件)
         W4=v[i]+v[q1[i]]+v[q2[i]] (买主件和那两个附件)
         设计一个状态opt[i]表示花i元钱可买到的物品的价格个重要度最大值。边界条件是opt[0]=0但是为了区分花i元钱是否可买到物品我们把初始条件opt[0]:=1;这样opt[i]>0说明花i元可以买到物品。这样就不难设计出这个状态的转移方程来:
         opt[i]=max{opt[i],opt[i-wj]} ((i-wj>0) and (opt[i-wj]>0)) (0<j<=4)
         显然题目的解就是opt[1]到opt[n]中的一个最大值。但在输出是要注意将解减1。
         注:价格是10是整数倍所以读入数据是可以使n=n div 10,wi=wi div 10
参考程序:#include <iostream>#include <cstdio>#include  <algorithm>using namespace std;int n, m;int w[101], v[101], zhu[100]; //价格,重要度,主件附件int dp[3300],f1[101],f2[101]; // dp[i]表示花费i元可以买到的最大的价格*重要度的和int main(){ // 由于价格全部是10元的整数倍,可以除去10求解,输出时再乘上10    scanf("%d %d", &m, &n);    m /= 10;    for (int i = 1; i <= n; ++i) {        scanf("%d %d %d", &w[i], &v[i], &zhu[i]);        w[i] /= 10; v[i] *= w[i];        if (zhu[i] > 0) {            if (f1[zhu[i]] == 0) f1[zhu[i]] = i;            else if (f2[zhu[i]] == 0) f2[zhu[i]] = i;        }    }    for (int i = 1; i <= n; ++i)        if (zhu[i] == 0) { // 仅对主件考虑,有若干种选择            for (int j = m; j >= w[i]; --j) {                // 只买主件                dp[j] = max(dp[j], dp[j - w[i]] + v[i]);                // 买主件和第一件附件(如果有)                if (f1[i] > 0 && j >= w[i] + w[f1[i]]) dp[j] = max(dp[j], dp[j-w[i]-w[f1[i]]] + v[i] + v[f1[i]]);                // 买主件和第二件附件(如果有)                if (f2[i] > 0 &&  j >= w[i] + w[f2[i]]) dp[j] = max(dp[j], dp[j-w[i]-w[f2[i]]] + v[i] + v[f2[i]]);                // 买主件和两件附件(如果有)                if (f2 > 0 && j >= w[i] + w[f1[i]] + w[f2[i]])                     dp[j] = max(dp[j], dp[j-w[i]-w[f1[i]]-w[f2[i]]]+v[i]+v[f1[i]]+v[f2[i]]);            }        }    printf("%d\n", dp[m]*10);}

0 0
原创粉丝点击