背包问题做题总结(十)(5.15)

本周学习心得:本周学习了二分法,还练习了一些背包问题,关于二分我以前了解过一点,数学上也学过一点二分,还不太会应用,等做一些题理解的更透彻了再写一篇详细的总结。关于背包问题,最近练的一些题目,基本上只要看出是哪一种类型,大体思路上就已经确定了,再结合题意做出一些改动。出错大部分都是因为一些细节问题,比如精度问题,初始化问题,限制条件等,少部分是思路想错了。背包问题有几种题型:求方案数,求最大最小值等,下面做一些详细的汇总。

01背包

01背包知识点回顾
C
题意:Roy去抢银行,告诉你浮点数P,低于概率P,Roy就会被抓,给你N表示他计划去的银行数,给你N行每行有两个量银行的钱mj,该银行被抓的概率pj,问他最多能得多少钱。

错误思想:一开始的思路是01背包,外循环循环银行数,内循环循环被抓概率,但因为浮点数不能做为数组下标,所以我一开始对所有概率先乘以100,想把它变整数。但这样做不对,一是乘以100后的精度还不是很精确,二是内循环我写的for(j=P*100; j>=a[i].p; j--)循环概率的思路上有问题。(不知道当时咋想的)
正解:反过来想,不能循环概率就循环钱,范围的最大值是所有的钱都能得到,所以要先把所有的钱求出来,dp[i]就表示得到i钱不被抓的概率,同样还是01的思想,每求一次乘以不被抓的概率就是他得到i钱不被抓的概率,最后找出不被抓概率情况下最大钱即可,注意初始条件,dp[0]=1.0,表示没有得到任何钱一定不被抓,因为求不被抓概率的最大值,所以dp数组其他都赋0。

#include<iostream>
#include<cstring>
#include<minmax.h>
using namespace std;
struct yh
{
    int m;
    double p;
} a[10005];
int i,j,t,n,x,sum;
double P,d[10005];
int main()
{
    cin>>t;
    while(t--)
    {   memset(d,0,sizeof(d));
        sum=0;
        cin>>P>>n;
        for(i=1; i<=n; i++)
        {
            cin>>a[i].m>>a[i].p;
            sum+=a[i].m;
        }
        d[0]=1.0;
        for(i=1; i<=n; i++)
        {
            for(j=sum; j>=a[i].m; j--)
            {
                d[j]=max(d[j],d[j-a[i].m]*(1-a[i].p));
            }
        }
        for(i=sum; i>=0; i--)
        {
            if(d[i]>=(1-P))
            {   cout<<i<<endl;
                break;
            }
        }
    }
    return 0;
}

F
题意:给出n个物品及每个物品的价格q[i],现在有m元钱问最多买多少种物品,并求出有多少种方案;(模板题)

01背包,内循环倒着循环钱数,用d[]记录物品种类,f[]记录方案数,种类转移方程比较好确定,d[j]=max(d[j],d[j-q[i]]+1)表示j钱最多能买多少种物品,在此基础上求方案数,如果d[j]<d[j-q[i]]+1,物品数是在前一个方案的基础上加1的,说明和前面是同一个方案,f[j]=f[j-q[i]]总方案数不变,d[j]==d[j-q[i]]+1,说明是不同的方案,f[j]+=f[j-q[i]]总方案数为当前方案加之前方案。
小知识memset只能用来填充char型数组,(因为只有char型占一个字节)如果填充int型数组,除了0和-1,还有0x3f3f3f3f,其他的不能。因为只有00000000 = 0,-1同理,如果我们把每一位都填充“1”,会导致变成填充入“11111111”。
memset()函数能将int型数组初始化为INF(0x3f3f3f3f),因为 int 数据类型是四个字节,memset 按字节赋值,memset(a, 0x3f, sizeof(a)) 意思就是把a数组中所有字节都赋值为 0x3f,0x3f 是正值,所以补码和原码一样,这样对 a 中的任意一个数据 a[i],就都变成了 0x3f3f3f3f。(计算机中存储所有数据都是补码,正数的补码是其二进制本身,负数是除符号位取反加 1 所以 -1 的补码是全 1)
fill函数可以赋任何值,使用方法为fill(vector.begin(), vector.end(), val)

参考 参考

#include<iostream>
#include<cstring>
#include<minmax.h>
using namespace std;
int d[100005],q[100005],f[100005];
//f方案数,d纪念品种类数
int main()
{
    int t,n,m,i,j;
    cin>>t;
    while(t--)
    {
        cin>>n>>m;
        fill(f,f+m,1);
        memset(d,0,sizeof(d));
        for(i=1; i<=n; i++)
            cin>>q[i];
        for(i=1; i<=n; i++)
        {
            for(j=m; j>=q[i]; j--)
            {
                if(d[j]<d[j-q[i]]+1)
                    f[j]=f[j-q[i]];
                else if(d[j]==d[j-q[i]]+1)
                    f[j]+=f[j-q[i]];
                d[j]=max(d[j],d[j-q[i]]+1);
            }
        }
        if(d[m])
            cout<<"You have "<<f[m]<<" selection(s) to buy with "<<d[m]<<" kind(s) of souvenirs."<<endl;
        else cout<<"Sorry, you can't buy anything."<<endl;
    }
    return 0;
}

D
题意: 给定背包容量,骨头的个数和每个骨头的价值,求在背包容量内,可以装的第k大价值,如果没有第k个最大值,那么输出0。

求第k大值,需要多一维记录k,处理方式见以下代码解释。

for(i=1; i<=n; i++)
{
for(j=v; j>=b[i]; j–)
{ int s=0;
for(int h=1; h<=k; h++)
{ c[s++]=d[j-b[i]][h]+a[i];
c[s++]=d[j][h];
//这里记录两种可能情况,可以装或不装,但注意不能用c[s++]=max(d[j-b[i][h]+a[i],d[j][h])直接代表以上两式,必须分开记录,否则就会把本来较大的数忽略,因为max只取每种里的最大值,就可能使这种情况里的较小值本来应该大于其他情况里的最大值,但是被忽略了。
}
sort(c,c+s,cmp);//把所以情况的值从大到小排序
int p=unique(c,c+s)-c;//去重考虑第k大值
for(int h = 1; h <= min(p, k); h++)//注意h的范围
d[j][h]=c[h-1];//把每一k大的值赋给d[j][h],更新数组的值,下一次求的时候每一k都不一样了。
}
}

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
int a[1005],b[1005],d[1005][35],c[1005];
bool cmp(int a,int b)
{
    return a>b;
}
int main()
{
    int t,i,n,v,j,k;
    cin>>t;
    while(t--)
    {
        cin>>n>>v>>k;
        for(i=1; i<=n; i++)
            cin>>a[i];
        for(i=1; i<=n; i++)
            cin>>b[i];
        memset(d,0,sizeof(d));
        for(i=1; i<=n; i++)
        {
            for(j=v; j>=b[i]; j--)
            {   int s=0;
                for(int h=1; h<=k; h++)
                {   c[s++]=d[j-b[i]][h]+a[i];
                    c[s++]=d[j][h];
                    //这里记录两种可能情况,可以装或不装,但注意不能用c[s++]=max(d[j-b[i][h]+a[i],d[j][h])直接代表以上两式,必须分开记录,否则就会把本来较大的数忽略,因为max只取每种里的最大值,就可能使这种情况里的较小值本来应该大于其他情况里的最大值,但是被忽略了。
                }
                sort(c,c+s,cmp);//把所以情况的值从大到小排序
                int p=unique(c,c+s)-c;//去重考虑第k大值
                for(int h = 1; h <= min(p, k); h++)//注意h的范围
                    d[j][h]=c[h-1];//把每一k大的值赋给d[j][h],更新数组的值,下一次求的时候每一k都不一样了。
            }
        }
        cout<<d[v][k]<<endl;
    }
    return 0;
}

完全背包

完全背包知识点回顾
H
题意:给你面值有1,5,10,25,50的币种,然后告诉你钱的数目,问用这些面值刚好凑成这个钱的方法有多少个。

典型的完全背包问题,求方案数,当个模板吧。以11为例,建议先看代码,再看图片解释。
在这里插入图片描述

#include<iostream>
#include<cstring>
using namespace std;
int v[6]= {1,5,10,25,50},d[100005];
int main()
{
    int n,i,j;
    while(cin>>n)
    {
        memset(d,0,sizeof(d));
        d[0]=1;
        for(i=0; i<5; i++)
        {
            for(j=v[i]; j<=n; j++)
            {   d[j]+=d[j-v[i]];
            }
        }
        cout<<d[n]<<endl;
    }
    return 0;
}

I
题意:
给你$100, $50, $20, $10, $5 的纸钞和$2, $1, 50c, 20c, 10c, 5c 的硬币。给你某金额的数字,问有多少种不同的方法可以组合成这个金额。

和上一个题做法一样,不过这个题数为浮点数,涉及到精度问题,而且浮点数不能做数组下标。解决:第一步,浮点数转整数,扩大100倍。第二步,提高准确度,降低误差,题目可能会给出0.205,这样的数,要求输入的数n要扩大100倍后加0.5进行4舍5入,如果不四舍五入,int a=n*100时会向下取整导致0.205相当于变成了0.20,使不能拼的数可以拼,答案错误。
小知识做题还发现了一些精度问题,画重点,要考的!
c++默认的流输出数值有效位是6,包括整数和小数,若数值超出6位,则第七位四舍五入到6位数
关于浮点数转整数,如:double n;int a=n*100,这里的a向下取整,如,0.209会变成20。
关于 fixed :浮点值显示为定点十进制。 默认是小数6位数,不包含整数,若小数位超出6位,则四舍五入到6位数。fixed 必须与setprecision(n)配合使用,用来控制小数位数,fixed与setprecision谁先谁后没有关系,但通常是fixed在前先固定6位小数(若此时小数已经超出6位,则先四舍五入到6位)再precision(n)取n位小数(n<6)。如果要控制的小数位数小于小数位,如:0.209,n=2,则输出结果为0.21

参考

#include<iostream>
#include<iomanip>
#include<string>
#include<cstring>
using namespace std;
long long int v[15]= {5,10,20,50,100,200,500,1000,2000,5000,10000};
long long int d[50005];
long long int i,j;
int main()
{
    double n;
    int a;
    while(cin>>n)
    {
        if(n==0.00)
            break;
        memset(d,0,sizeof(d));
        d[0]=1;
        a=n*100+0.5;
        for(i=0; i<11; i++)
        {
            for(j=v[i]; j<=a; j++)
            {
                d[j]+=d[j-v[i]];
            }
        }
        cout<<setw(6)<<fixed<<setprecision(2)<<n<<setw(17)<<d[a]<<endl;
    }
    return 0;
}

J
题意:给你两个数n,k,让你用1到k这k个数表示n,问有几种方法(大精度)

思路比较好想,但得到的结果可能相当大,超过了long long,所以要解决这个问题。看了一些其他人的解释,这个题最大的数字为33位,我们可以将两个long long的数字进行拼接,组成一个超过33位的数。也就是用两个 long long 型来表示一个大数,分别是高位和低位,低位不超过10^18,也就是有17位数。以下代码中,a[i]表示低位,d[i]表示高位。(高位我还没想清楚怎么组成的33位数,先学一下这种方法,欢迎大神解答疑惑)

#include<iostream>
#include<cstring>
#include<string>
//#include<minmax.h>
using namespace std;
const long long int maxx=1e18;
long long int d[100005],a[100005];
int main()
{
    long long int i,j,n,k;
    cin>>n>>k;
    memset(d,0,sizeof(d));
    memset(a,0,sizeof(a));
    a[0]=1;
    for(j=1; j<=k; j++)
    {
        for(i=1; i<=n; i++)
        {
            if(i-j>=0)
            {   d[i]=d[i]+d[i-j]+(a[i]+a[i-j])/maxx;
                a[i]=(a[i]+a[i-j])%maxx;
                //cout<<"i-j="<<i-j<<endl;
                //cout<<"i="<<i<<" d[i]="<<d[i]<<endl;
            }
        }
    }
    if(d[n])
        cout<<d[n];
    cout<<a[n]<<endl;
    return 0;
}

M
题意:给出本金和年数,又给出几种股票的价钱和利息,求最大本利和,每种股票可以多次购买。(债券的价值总是1000美元的倍数。债券的利息从来不超过其价值的10%。)

不难看出是完全背包,一些细节需注意,债券的价值是1000美元的倍数,所以存数组的时候可以先除以1000,以防最后数太大超范围,每年的利息要单独求,求得的利息加上原来的钱作为下一次的本金。(一开始没明白为什么可以这样求,觉得原来的钱买了股票后应该变少了,求得的利息加上原来的钱做为下一次本金好像不太合理,就去百度了一下什么是本利和,就是本金与利息的和,有一个复利计算公式是计算前一期利息再生利息的问题,计入本金重复计息,即“利生利”“利滚利”。它的的特点是:把上期末的本利和作为下一期的 本金,在计算时每一期本金的数额是不同的。主要应用于计算多次等额投资的本利终值和计算多次等额回款值。

#include<iostream>
#include<cstring>
using namespace std;
struct z
{
    int v,l;
} a[11];
int d[100005],i,j,k;
int t,m,n,p,sum;
int main()
{
    cin>>t;
    while(t--)
    {
        cin>>m>>n>>p;
        for(i=1; i<=p; i++)
        {   cin>>a[i].v>>a[i].l;
            a[i].v=a[i].v/1000;
        }
        sum=m;
        for(i=1; i<=n; i++)
        {   m=sum/1000;
            memset(d,0,sizeof(d));
            for(j=1; j<=p; j++)
            {   for(k=a[j].v; k<=m; k++)
                {
                    d[k]=max(d[k],d[k-a[j].v]+a[j].l);
                }
            }
            sum+=d[m];
            m=sum;
        }
        cout<<sum<<endl;
    }
    return 0;
}

多重背包

P
题意:奶牛要用石头上天,给出每一块石头的高度,不可超过的高度数量,问奶牛最多可以达到的高度

必须先贪心,按照石头从小到大排序,这样才能尽可能高,如果不排序,可能导致两个大的石头搭配,后面的小石头不能再叠,但如果排序了,先叠小石头,再叠大石头,可能正好到那个高度,反而更合适。剩下的按完全背包的套路求就行。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
struct s
{
    int h,a,c;
    bool operator <(const s &b) const
    {
        return a<b.a;
    }
} n[500000];
int d[500000],x[500000];
int main()
{
    int k,i,j;
    cin>>k;
    for(i=1; i<=k; i++)
    {
        cin>>n[i].h>>n[i].a>>n[i].c;
    }
    sort(n+1,n+k+1);
    d[0]=1;
    int ans=0;
    for(i=1; i<=k; i++)
    {   memset(x,0,sizeof(x));
        for(j=n[i].h; j<=n[i].a; j++)
        {//下面这一步判断是确保该高度没到达并且前一高度到达了,石头数还够
            if(d[j]==0&&d[j-n[i].h]==1&&n[i].c>x[j-n[i].h])
            {   d[j]=1;
                x[j]=x[j-n[i].h]+1;//可以就用掉一块石头
                ans=max(ans,j);
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

分组背包

Y
题意:有S款运动鞋,一个n件,总钱数为m,求不超过总钱数且每款鞋子至少买一双的情况下,使价值最大。如果有一款买不到,就输出“Impossible"。

把所有鞋子按品牌分组,每一组里的鞋子单独看成一种,即按01背包求。详细见代码解释。

#include<iostream>
#include<minmax.h>
using namespace std;
int N,M,K,i,j,k;
int d[10005][10005];
//产品总数,钱,品牌数
struct p
{
    int a,b,c;//品牌号,价格,价值
} x[100005];
int main()
{
    while(cin>>N>>M>>K)
    {   for(i=1; i<=N; i++)
            cin>>x[i].a>>x[i].b>>x[i].c;
        for(i=0; i<=K; i++)
        {
            for(j=0; j<=M; j++)
            {
                if(i==0)//品牌数为0,无论有多少钱,最大价值都是0;
                    d[i][j]=0;
                else d[i][j]=-1;
            }
        }
        for(i=1; i<=K; i++)//品牌数(组数)循环
        {
            for(j=1; j<=N; j++)//产品总数
            {
                if(x[j].a==i)//如果是该组成员
                {
                    for(k=M; k>=x[j].b; k--) //01思想,买或不买
                    {   d[i][k]=max(d[i][k],d[i][k-x[j].b]+x[j].c);
                        d[i][k]=max(d[i][k],d[i-1][k-x[j].b]+x[j].c);
                    }
                }
            }
        }
        if(d[K][M]<0) cout<<"Impossible"<<endl;
        else
            cout<<d[K][M]<<endl;
    }
    return 0;
}

暂时写到这里,有新题型再继续补充_(:з」∠)_

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值