01背包

转载来自:http://blog.csdn.net/libin56842/article/details/9338841

首先01背包题目的雏形是

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

从这个题目中可以看出,01背包的特点就是:每种物品仅有一件,可以选择放或不放。

其状态转移方程是:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

对于这方方程其实并不难理解,方程之中,现在需要放置的是第i件物品,这件物品的体积是c[i],价值是w[i],因此f[i-1][v]代表的就是不将这件物品放入背包,而f[i-1][v-c[i]]+w[i]则是代表将第i件放入背包之后的总价值,比较两者的价值,得出最大的价值存入现在的背包之中。

理解了这个方程后,将方程代入实际题目的应用之中,可得

for(i = 1; i<=n; i++)  

 

{  
    for(j = v; j>=c[i]; j--)//在这里,背包放入物品后,容量不断的减少,直到再也放不进了  
    {  
        f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i]);  
    }  
}  

 

 

 

 

 

 

理解了01背包之后,下面就来看看实际的题目

 

 

HDU2546:饭卡
http://acm.hdu.edu.cn/showproblem.php?pid=2546

 

很经典的一道01背包题,要注意的是这里只要剩余的钱不低于5元,就可以购买任何一件物品,所以5在这道题中是很特许的,再使用01背包之前,我们首先要在现在所拥有的余额中拿5元去购买最贵的,而剩下的钱就是背包的总容量,可以随意使用。但是需要排个序从小到大(因为需要从范围大的更新到范围小的)。

 

 

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n;
int c[1100],dp[1100];
int main()
{
    while(~scanf("%d",&n))
    {
        if(n==0)
            break;
        int sum=0;
        for(int i=0; i<n; i++)
            scanf("%d",&c[i]);
        int m;
        scanf("%d",&m);
        memset(dp,0,sizeof(dp));
        int ans=0;
        sort(c,c+n);
        if(m<5)
            printf("%d\n",m);
        else
        {
            m-=5;
            for(int i=0; i<n-1; i++)
            {
                int flag=0;
                for(int j=m; j>=c[i]; j--)
                {
                    dp[j]=max(dp[j-c[i]]+c[i],dp[j]);
                    ans=max(ans,dp[j]);
                }
            }
            printf("%d\n",m+5-c[n-1]-ans);
        }
    }
    return 0;
}



 

 

 

 

 

 

HDU1171:Big Event in HDU

http://acm.hdu.edu.cn/showproblem.php?pid=1171

这道题咋看有点复杂,其实也只是换了一种思维,因为题目要求要尽量平均分配,所以我们可以先将总价值sum求出,然后得出其分配的平均值为sum/2,要注意这个答案可能为小数,但是又因为sum是整数,所以最后得出的sum/2是要小于等于实际的值。将这个结果进行01,背包,可以得出其中一个宿舍dp[sum/2]所得的最大价值,而另一个宿舍的最大价值也可以相应的得到,而前者必定小于等于后者(因为sum/2小于等于实际值)。

 

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n;
int v[5500],dp[255000];
int main()
{
    while(~scanf("%d",&n))
    {
        if(n<=-1)
            break;
        int m=0;
        int k=0;
        for(int i=0; i<n; i++)
        {
            int x,y;
            scanf("%d %d",&x,&y);
            while(y--)
            {
                m+=x;
                v[k++]=x;
            }
        }
        int  mm=m/2;
        int ans=0;
        memset(dp,0,sizeof(dp));
        for(int i=0;i<k;i++)
            for(int j=mm;j>=v[i];j--)
        {
            dp[j]=max(dp[j],dp[j-v[i]]+v[i]);
            ans=max(ans,dp[j]);
        }
        int cnt=m-ans;
        if(ans>cnt)
            printf("%d %d\n",ans,cnt);
        else
            printf("%d %d\n",cnt,ans);
    }
    return 0;
}

 

 

 

 

 

HDU2602:Bone Collector

http://acm.hdu.edu.cn/showproblem.php?pid=2602

经典的01背包题,给出了石头的数量与背包的容量,然后分别给出每个石头的容量与价值,要求最优解,经过前面的练手,这道题已经是很简单了,可以说是01背包果题。标准的模板题。

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int t,n,m;
int c[1100],v[1100];
int dp[1100];
int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++)
        scanf("%d",&c[i]);
        for(int i=0;i<n;i++)
            scanf("%d",&v[i]);
        memset(dp,0,sizeof(dp));
        for(int i=0;i<n;i++)
            for(int j=m;j>=v[i];j--)
            dp[j]=max(dp[j],dp[j-v[i]]+c[i]);
            printf("%d\n",dp[m]);
    }
    return 0;
}

 

 

 

 

 

 

 

HDU2639:Bone Collector II(01背包第k优解)

http://acm.hdu.edu.cn/showproblem.php?pid=2639

解决了上面那倒题目之后,这道题跟上面的题目有些不同,因为这里要求的是第K优解

 

求第K优解

对于求次优解、第K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。其基本思想是将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。这里仍然以01背包为例讲解一下。首先看01背包求最优解的状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。如果要求第K优解,那么状态f[i][v]就应该是一个大小为K的数组f[i][v][1..K]。其中f[i][v][k]表示前i个物品、背包大小为 v时,第k优解的值。“f[i][v]是一个大小为K的数组”这一句,熟悉C语言的同学可能比较好理解,或者也可以简单地理解为在原来的方程中加了一维。 显然f[i][v][1..K]这K个数是由大到小排列的,所以我们把它认为是一个有序队列。然后原方程就可以解释为:f[i][v]这个有序队列是由f[i-1][v]和f[i-1][v-c[i]]+w[i]这两个有序队列合并得到的。有序队列f[i-1][v]即f[i-1][v][1..K],f[i-1][v-c[i]]+w[i]则理解为在f[i-1][v-c[i]] [1..K]的每个数上加上w[i]后得到的有序队列。合并这两个有序队列并将结果的前K项储存到f[i][v][1..K]中的复杂度是O(K)。最后的答案是f[N][V][K]。总的复杂度是O(VNK)。

 

为什么这个方法正确呢?实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其 它在任何一个策略上达不到最优的方案都被忽略了。如果把每个状态表示成一个大小为K的数组,并在这个数组中有序的保存该状态可取到的前K个最优值。那么, 对于任两个状态的max运算等价于两个由大到小的有序队列的合并。另外还要注意题目对于“第K优解”的定义,将策略不同但权值相同的两个方案是看作同一个解还是不同的解。如果是前者,则维护有序队列时要保证队列里的数没有重复的。

 

用个形象的比喻吧:如果我想知道学年最高分,那么,我只要知道每个班级的最高分,然后统计一遍就可以了。如果我想知道学年前十呢?我必须要知道每个班的前十名。大家在心里模拟一下,对,这就是本题核心的算法。两种决策,就可以看作这个学年只有两个班。

 

这里分别用a,b数组来存储

 a[l]=dp[j-v[i]][l]+c[i];
 b[l]=dp[j][l];

 

把所有的结果存下来再去寻找第k个最优解

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int t,n,m,k;
int c[110],v[110],dp[1100][35];
int a[35],b[35];
int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d%d",&n,&m,&k);
        for(int i=1; i<=n; i++)
            scanf("%d",&c[i]);
        for(int i=1; i<=n; i++)
            scanf("%d",&v[i]);
        memset(dp,0,sizeof(dp));
        int i,j,l;
        for(i=1; i<=n; i++)
        {
            for(j=m; j>=v[i]; j--)
            {
                for(l=1; l<=k; l++)
                {
                    a[l]=dp[j-v[i]][l]+c[i];
                    b[l]=dp[j][l];
                }
                a[l]=b[l]=-1;
                int ii,jj,kk;
                ii=jj=kk=1;
                while(kk<=k&&(a[ii]!=-1||b[jj]!=-1))
                {
                    if(a[ii]>b[jj])
                    {
                        dp[j][kk]=a[ii];
                        ii++;
                    }
                    else
                    {
                        dp[j][kk]=b[jj];
                        jj++;
                    }
                    if(dp[j][kk]!=dp[j][kk-1])
                        kk++;
                }
            }
        }
        printf("%d\n",dp[m][k]);
    }
    return 0;
}

 

 

 

 

 

HDU2955:Robberies

http://acm.hdu.edu.cn/showproblem.php?pid=2955

(题目的背景是一个人想去抢多家银行)首先给定一个数T,表示的是有T组数据,每组数据首先给出一个小数p和一个整数n,分别表示的是最大的能够被抓住的概率,如果>这个概率这个强盗的母亲就不让他去,然后 下面有n行数据,每行有两个数,分别表示这个银行的钱数和被抓住的概率,让你求的是这个强盗在不被抓住的概率的条件下,能够抢到的最大钱数。

这道题有点特别,咋看之下其状态转移方程似乎有些不同,但事实上远离是相通的,要注意其精度,比较大小的时候不能直接判等dp[i][j]  i表示哪一种第i个银行,j表示钞票数,合起来表示抢到第i种银行,偷到j张钞票的被抓的概率

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
double b;
int n;
int m[110];
double p[110];
double  dp[11110];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%lf%d",&b,&n);
        b=1-b;
        int sum=0;
        for(int i=0; i<n; i++)
        {
            scanf("%d%lf",&m[i],&p[i]);
            p[i]=1-p[i];
            sum+=m[i];
        }
        memset(dp,0.0,sizeof(dp));
        dp[0]=1.0;
        for(int i=0; i<n; i++)
            for(int j=sum; j>=m[i]; j--)
                dp[j]=(double)max(dp[j],dp[j-m[i]]*p[i]);
        for(int i=sum; i>=0; i--)
        {
            //printf("%.8f  %.8f\n",dp[i],b);
            if(dp[i]-b>0.00000000001)
            {
                printf("%d\n",i);
                break;
            }
        }
    }
    return 0;
}

 

 

 

 

 

HDU3466:Proud Merchants

http://acm.hdu.edu.cn/showproblem.php?pid=3466

给你n种东西,你手上有m元,每种东西购买需要p元,你手里至少需要q元才能购买,所得的价值为v

这道题由于规定了手上的前低于q时就不能购买该样东西,所以要先将商品按q-p排序(因为先更新范围大的,就是更新到一个最小的数字所以是从小到大),剩下的就是简单的01背包了

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,m;
struct node
{
    int p,q,v;
} a[600];
int dp[6000];
bool cmp(node aa,node bb)
{
 return aa.q-aa.p < bb.q-bb.p;
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=0; i<n; i++)
            scanf("%d%d%d",&a[i].p,&a[i].q,&a[i].v);
        sort(a,a+n,cmp);
        int t=m;
        int ans=0;
        memset(dp,0,sizeof(dp));
        for(int i=0; i<n; i++)
        {
            for(int j=m; j>=a[i].q; j--)
            {
                dp[j]=max(dp[j],dp[j-a[i].p]+a[i].v);
                ans=max(ans,dp[j]);
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

 

 

 

 

 

HDU1864:最大报销额

http://acm.hdu.edu.cn/showproblem.php?pid=1864

题目中药注意的有几样,首先每张发票中单件物品价格不能超过600,其次发票总额不能超过1000,而且发票上的物品必须是ABC三类,将满足以上条件的发票存入数组之中,就是裸01背包。需要分别记录每一张钞票内A,B,C类物品的总额,然后用01背包去选择报销哪些发票。这里数字都是浮点型*100换成整型就可以了。

 

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
double q,x;
int n,m,y,aa,bb,cc,xx;
char s;
int a[50],dp[5000000];
int main()
{
    while(~scanf("%lf %d",&q,&n)&&n)
    {
        y=(int)(q*100);
        memset(dp,0,sizeof(dp));
        memset(a,0,sizeof(a));
        for(int i=0; i<n; i++)
        {
            scanf("%d",&m);
            int flag=0;
            aa=0,bb=0,cc=0;
            for(int j=0; j<m; j++)
            {
                scanf(" %c:%lf",&s,&x);
                xx=(int)(x*100);
                if(s=='A')
                aa+=xx;
                else if(s=='B')
                bb+=xx;
                else if(s=='C')
                cc+=xx;
                else
                    flag=1;
            }
            if(aa+bb+cc>100000||aa>60000||bb>60000||cc>60000)
                flag=1;
            if(flag==1)
                a[i]=0;
                else
            a[i]=aa+bb+cc;
        }
        for(int i=0; i<n; i++)
            for(int j=y; j>=a[i]; j--)
                dp[j]=max(dp[j],dp[j-a[i]]+a[i]);
        printf("%.2f\n",(dp[y]/100.0));
    }
    return 0;
}

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值