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