今天要给大年初一今天正月十三接近两个周的背包练习画一个句号。
采药写多了的人表示总结出来就一句话:不是所有的背包题都有这样一行或者类似这样一行状态转移方程
dp[j]=max(dp[j],balabala);//皮一下很开心
板子题就不放了
快读快输就不写了
引用题目均来自洛谷
注意:枚举重量时,如果开一维数组,01背包要倒序枚举,完全背包要正序枚举;
01背包开二维数组时可以正向枚举。
忍不住放01背包二维数组和一维数组的核心代码
//二维
for (int i=1; i<=m; i++)
for (int j=1; j<=t; j++) {
//for(int j=v[i]
f[i][j] = f[i-1][j];// 当前重量装不进,价值等于前i-1个物品
if (j >= v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]); // 能装,需判断 是放这个物品还是不放价值大
}
//一维
for (int i=1; i<=m; i++)
for (int j=t; j>=v[i]; j--) {
f[j] = max(f[j],f[j-v[i]]+w[i]); // 能装,需判断 是放这个物品还是不放价值大
}
第一类 裸背包题
1.P2639 [USACO09OCT]Bessie的体重问题Bessie’s We…
USACO 2009
//看不见英文全名就很难受(滑稽
//题目所给数据均在Int范围内
//每捆干草只能被吃一次(即使在列表中相同的重量可能出现2次,但是这表示的是两捆干草,其中每捆干草最多只能被吃掉一次) 这句告诉我们:这是一个01背包。
//当做价值和重量相等的01背包处理即可。
#define maxn 10005
int h,n,s[maxn],f[maxn],ans;
int main(){
h=read(),n=read();
for(int i=1;i<=n;i++)
s[i]=read();
for(int i=1;i<=n;i++)
for(int j=h;j>=s[i];j--){
f[j]=max(f[j],f[j-s[i]]+s[i]);
}
for(int i=1;i<=h;i++)
ans=max(ans,f[i]);
write(ans);
二、稍有变形
1.P1164 小A点菜
#define maxn 10005
//每种菜品只有1份,要求正好花完m元,即一个"正好装满"的01背包。
int dp[maxn][maxn],v[maxn],n,m;
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)
v[i]=read();
for(int i=1;i<=n;i++)//开始选择菜品
for(int j=1;j<=m;j++){
//正向枚举,枚举的是钱数
if(j<v[i])//买不起这道菜
dp[i][j]=dp[i-1][j];//实现当前钱数的点菜方案数和选到上一道菜时产生的方案数相同
if(j==v[i])//刚好买得起
dp[i][j]=dp[i-1][j]+1;//在经过上一道菜后产生的方案数上+1
//+1是因为其他组合可以达到j元,买这一道菜也刚好达到j元,买这一道菜算一个方案
if(j>v[i])//可以买完这道菜还有余额
dp[i][j]=dp[i-1][j]+dp[i-1][j-v[i]];//到上一道菜为止实现当前金额产生的方案数+
//看过上一道菜后已经花费的钱同点了这道菜金额一样的方案数
}
//一定要明确i和j的含义
write(dp[n][m]);
return 0;
}
2.P1832 A+B Problem(再升级)
P2563 [AHOI2001]质数和分解
两个题意是一模一样的,但是省选题数据强大一点,反正我的质数和分解被卡了输出
//P2563 [AHOI2001]质数和分解
int s[201],f[201];
bool ps(int x){
//筛法判断素数
for(int i=2;i<=sqrt(x);i++)
if(x%i==0) return 0;
return 1;
}
int main(){
int n;
while(cin>>n){
int ans=0;
for(int i=2;i<=n;i++)//1不是质数不是合数
if(ps(i))
s[++ans]=i;
memset(f,0,sizeof(f));
f[0]=1;//重要但是易被忽略
for(int i=1;i<=ans;i++)
{
for(int j=u[i];j<=200;j++)
f[j]+=f[j-s[i]];//1个数拆成若干个素数和的方案数,
//等同于拆成所有该数减去一个素数的差的方案数之和
}
cout<<f[n]<<endl;
}
return 0;
}
3.P1358扑克牌
//应用到乘法原理和组合数学
//边执行边取模能减小炸空间的可能,但是sum和dp[][]还是要开long long
int n,m,x;
long long sum=1;
long long dp[maxn][maxn];//i张牌中已经取了j张
#define mod 10007
int main(){
n=read(),m=read();
for(int i=0;i<=n;i++)
dp[i][0]=1;
for(int i=1;i<=min(n,m);i++)
dp[i][i]=1;
for(int i=1;i<=n;i++)//i张牌中已经取了j张
for(int j=1;j<=min(n,100);j++)//j上限的约束是因为每个人可能有0张可能有1张可能有多张牌
//牌较少时每个人最多拿n张牌,牌足够多时每个人最多100张牌
dp[i][j]=(dp[i-1][j-1]+dp[i-1][j])%mod;//组合数性质
for(int i=1;i<=m;i++){
x=read();
sum*=dp[n][x];
sum=sum%mod;
n-=x;
}
write(sum);
return 0;
}
4.P1877 [HAOI2012]音量调节
//题面是可以看到背包的影子的。
//这个题不需要"价值"
//音量即“重量”
//"调高调低"——“物品重量”为正和负
#define MAXN 1005
using namespace std;
int n,begi,maxn,v[MAXN],f[MAXN][MAXN];
int main(){
read(n),read(begi),read(maxn);
f[0][begi]=1;//初始化,第1首歌开始需要的音量一定能实现。
for(int i=1;i<=n;i++)
read(v[i]);
for(int i=1;i<=n;i++)
for(int j=begi;j<=maxn;j++){
if(f[i-1][j])//选择调高还是调低
f[i][j+v[i]]=1,f[i][j-v[i]]=1;
}
for(int i=maxn;i>=0;i--)
if(f[n][i]){
//找到最后能达到的最大音量
write(i);
return 0;
}
printf("-1");//不要学司延把else写在for外面的傻写法
return 0;
}
5.P5662 纪念品
CSP-J 2019
int t,n,m,pre[maxn],pres[maxn],f[maxn],del[maxn],ans;
//核心思想:判断第二天商品是否涨价,并在第一天购买
//相当于多重背包,但开始手中的钱数只用于判断“是否买得起”
int main(){
f[0]=m;//f[0]可初始化为任何正数,保证第一天有购买东西的可能
t=read(),n=read(),m=read();
for(int i=1;i<=t;i++){
for(int j=1;j<=n;j++){
scanf("%d",&pre[j]);//当天的价格
pres[j]=del[j];//前一天的物品价格
del[j]=pre[j]-pres[j];//计算差价并视为物品价值
}
if(i>1){
//至少从第二天开始“赚差价”
memset(f,0,sizeof(f)); //每天都只按照差价规划,与昨天的价格变动无关,所以每天从0开始
for(int b=1;b<=n;b++)//按物品遍历//c>=0&&
for(int c=pres[b];c<=m;c++){
//开始判断物品是否应该购买
if(f[c-pres[b]]>=0)//如果物品在第二天涨价且手头有钱,就在第一天购买。
f[c]=max(f[c],f[c-pres[b]]+del[b]);
}
ans=0;
for(int i=0;i<=m;i++)
ans=max(ans,f[i]);
m+=ans;
}
for(int j=1;j<=n;j++)
del[j]=pre[j];//保留前一天的价格用以计算差价
}
write(m);
return 0;
}
6.P2946 [USACO09MAR]牛飞盘队Cow Frisbee Team
USACO 2009
//没什么难度,就是特别注意dp开成一维数组会炸内存而且浪费
#define maxn 2005
const int mod=1e8;
int r[maxn];
int dp[maxn][maxn];
int n,f;
int main(){
dp[0][0]=1;
n=read(),f=read();
for(int i=1;i<=n;i++){
r[i]=read();
for(int j=0;j<=f;j++){
dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
dp[i][j]=(dp[i][j]+dp[i-1][(j+r[i])%f])%mod;
}
}
write(dp[n][f]);
return 0;
}
三、穿上马甲别让做题的看出来我是个背包
1.P1044 栈
还有一种应用卡特兰数的写法
#define maxn 1005
int f[maxn][maxn],n;
//这道题的数据强度写不写记忆化搜索都可以
inline int js(int x,int y){
///x:操作队列里元素的个数,y:栈里的个数
if(f[x][y])
return f[x][y];
if(x==0)
return 1;//队列里没有元素了,方案不能再发生变化
if(y>0) //栈里还有元素
f[x][y]+=js(x,y-1);//达到当前状态的方案数加上栈里弹出一个元素状态的方案数
f[x][y]+=js(x-1,y+1);//一定要进行的操作就是当前状态方案数+
//进栈一个(同时队列里少了一个元素)的方案数
return f[x][y];
}
int main(){
n=read();
write(js(n,0));
return 0;
}
2.P5020 货币系统
NOIP2018 提高组
#define maxn 100005
int t,coin[maxn],m,n,mon[maxn];
//思维的重点不在背包,实现侧重背包一些
//method 1,偏重数学思维
//其实就是看已知硬币面额中,较大金额能否被多个较小金额凑出。
int main(){
t=read();
while(t--){
n=read();
memset(coin,0,sizeof(coin));//多组数据注意每次都要初始化
memset(mon,0,sizeof(mon));
m=0;
for(int i=1;i<=n;i++)
coin[i]=read();
sort(coin+1,coin+1+n); //important
for(int i=1;i<=n;i++)
mon[coin[i]]=2;//用单枚原有硬币可以凑出的金额
for(int i=1;i<=coin[n];i++)//枚举金额
for(int j=1;j<=n;j++){
//枚举硬币
if(mon[i])//当前能凑出的面额,加一枚原有硬币也能凑出
if(i+coin[j]<=coin[n])//防止多余枚举
mon[i+coin[j]]=1;
}
for(int j=1;j<=coin[n];j++)
if(mon[j]==2)
m++;
write(m),printf("\n");
}
return 0;
}
//method 2 背包
#define maxn 1000005
int coin[maxn],mon[maxn];
int x,t,n,m;
int main(){
t=read();
while(t--){
n=read();
m=0;//运行多组数据的必要归零
memset(mon,0,sizeof(mon));
memset(coin,0,sizeof(coin));
for(int i=1;i<=n;i++)
coin[i]=read();
sort(coin+1,coin+1+n);
for(int i=1;i<=n;i++){
//外层按货币枚举,能凑出来的面额置为1
if(mon[coin[i]])//当前面额已经判断为1,进行下一重i循环
continue;
m++,mon[coin[i]]=1;//用单个已知货币能凑出的金额置1,且最小的几张一定要选
for(int j=coin[i];j<=coin[n];j++)//内层按金额枚举
if(mon[j-coin[i]])//不用这枚货币能凑出的金额,加上这枚货币可凑出一个新的金额
mon[j]=1;
}
write(m),printf("\n");
}
return 0;
}
总结起来就是em一定不要做单一来源和单一难度的题,不同题涉及“背包”的侧重不同,而且一定不要受模板限制。