题解 P1064 【金明的预算方案】
Mutsumi_0114
2018-10-20 17:06:38
### “有依赖的背包问题” [(背包九讲)](https://www.kancloud.cn/kancloud/pack/70131)
大部分题解做法,是将本题每项主件与其附件的每种搭配情况在递推式内枚举,使得代码递推式部分冗长臃肿,且不适于附件较多的情况。
本题属于 非树形有依赖的背包问题(只有两类物品:主件,附件),下面给出具体解题思路。
* 注意,真正的树型依赖背包更为复杂,以至于可计算的数据范围骤减
首先我们注意到对于每一个主件,有很多种购买的方案:可以不买,可以只买主件,或者买主件外加几种附件,当附件个数较少的时候枚举就可以 $AC$。
例如这样:
![](https://cdn.luogu.com.cn/upload/pic/39276.png)
(上图来自 @ShawnZhou 的题解)
本做法中,我们先对每种主件的 **附件的集合** 进行一次 $01$ 背包处理,就可以先求出 **对于每一种主件包括其附件的组合中,每种花费的最大价值**(读不懂的同学可以看后面解释)。
![](https://cdn.luogu.com.cn/upload/pic/39287.png)
对于每一种主件的01背包处理:
```
for i:主件k的所有附件
for j:价值(0 ~ n-主件价值)
01背包递推
```
这样可以得到主件 $k$ 的附件中费用依次为 $0 \sim n-v[k]$ 时的相应最大价值 $f[0 \sim n-v[k]]$,那么我们就得到了主件 $k$ 及其附件集合的 $n-v[k]+1$ 种不同选择情况,其中费用为 $v[k]+t$ 的物品的价值就是 $f[t]+v[k]*p[k]$ 。
![](https://cdn.luogu.com.cn/upload/pic/39289.png)
对于每一个主件处理出的情况,**在 $n-v[k]+1$ 种情况之中只能最多选择一种选入最终答案之中(把上面文字多读几遍吧)**,原问题便转化成一个分组背包问题。
如果你不知道分组背包的话:
```
for 所有的组k
for v = V ... 0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
```
这题的分组背包部分应该是这样写的:
```
for 所有的主件数k
for j = n ... 0
for 所有的主件和附件的组合属于组k
f[j]=max{f[j],f[j-v[i]]+v[i]*p[i]}
```
上面就已经是具体思路了,实现需要比较多的数组存储结果,还有最容易错的一点是,本题的情况状态01背包计算需要使用 “恰好背包” (数组下标严格与花费价钱相等,详见代码注释):
```cpp
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct cas
{
int v,p,q;
}a[60],pat[60][60];
int n,m,t[60],V[60][10],P[60][10],cnt[60],f[32000],ans;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a[i].v,&a[i].p,&a[i].q);//正常的读入
if(a[i].q)//如果这个物品是附件
{
t[a[i].q]++;
pat[a[i].q][t[a[i].q]].v=a[i].v;
pat[a[i].q][t[a[i].q]].p=a[i].p;
pat[a[i].q][t[a[i].q]].q=a[i].q;//把它存在相应的主件的分组中
}
}
for(int i=1;i<=m;i++)//01背包处理
{
if(t[i])//如果当前物品为主件
{
memset(f,-1,sizeof(f));//恰好背包的处理,-1表示不恰好取到此价值
f[0]=0;//恰好背包的处理
for(int j=1;j<=t[i];j++)
for(int k=n-a[i].v;k>=pat[i][j].v;k--)
if(f[k]<f[k-pat[i][j].v]+pat[i][j].v*pat[i][j].p && f[k-pat[i][j].v]!=-1)//恰好背包的判断
f[k]=f[k-pat[i][j].v]+pat[i][j].v*pat[i][j].p;//很平常的01状态转移
for(int j=0;j<=n-a[i].v;j++)
if(f[j]!=-1)//恰好背包的判断,这种附件组合满足题意
{
cnt[i]++;
V[i][cnt[i]]=j+a[i].v;
P[i][cnt[i]]=f[j]+a[i].v*a[i].p;//把此情况存在主件i的分组中,为分组背包做好处理
}
}
if(!a[i].q)//只买主件
{
cnt[i]++;
V[i][cnt[i]]=a[i].v;
P[i][cnt[i]]=a[i].v*a[i].p;//存储
}
}
memset(f,0,sizeof(f));
for(int i=1;i<=m;i++)
for(int j=n;j>=0;j--)
for(int k=1;k<=cnt[i];k++)
if(j>=V[i][k])
f[j]=max(f[j],f[j-V[i][k]]+P[i][k]);//分组背包的计算
for(int i=0;i<=n;i++)
ans=max(ans,f[i]);
printf("%d",ans);//输出
return 0;
}
```
自制大数据练习:[U65320 Oak的预算方案](https://www.luogu.org/problemnew/show/U65320) (数据有误请及时反馈
------------
### 2019-8-3 更新:
在题目 $U65320$ 中, 收集到 @Jackpei 的优异解法, 供大家借鉴 (擅自修改了码风, 希望有助于各位阅读, 在此不作分析):
```cpp
#include <cstdio>
#include <algorithm>
using namespace std;
int n,m;
int f[5000010],h[5000010];
struct node
{
int v,p,q;
}a[1010];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&a[i].v,&a[i].p,&a[i].q);
a[i].p*=a[i].v;
}
for(int i=1;i<=m;i++)
if(!a[i].q)
{
for(int j=1;j<a[i].v;j++)
h[j]=0;
for(int j=a[i].v;j<=n;j++)
h[j]=f[j-a[i].v]+a[i].p;
for(int j=1;j<=m;j++)
if(a[j].q==i)
for(int k=n;k>=a[i].v+a[j].v;k--)
h[k]=max(h[k],h[k-a[j].v]+a[j].p);
for(int j=a[i].v;j<=n;j++)
f[j]=max(f[j],h[j]);
}
printf("%d\n",f[n]);
return 0;
}
```