洛谷P4037 [JSOI2008]魔兽地图 题解
题目链接:P4037 [JSOI2008]魔兽地图
题意:
DotR (Defense of the Robots) Allstars是一个风靡全球的魔兽地图,他的规则简单与同样流行的地图DotA (Defense of the Ancients) Allstars。
DotR里面的英雄只有一个属性——力量。他们需要购买装备来提升自己的力量值,每件装备都可以使佩戴它的英雄的力量值提高固定的点数,所以英雄的力量值等于它购买的所有装备的力量值之和。装备分为基本装备和高级装备两种。基本装备可以直接从商店里面用金币购买,而高级装备需要用基本装备或者较低级的高级装备来合成,合成不需要附加的金币。装备的合成路线可以用一棵树来表示。
比如,Sange and Yasha的合成需要Sange,Yasha和Sange and Yasha Recipe Scroll三样物品。其中Sange又要用Ogre Axe, Belt of Giant Strength和 Sange Recipe Scroll合成。每件基本装备都有数量限制,这限制了你不能无限制地合成某些性价比很高的装备。
现在,英雄Spectre有M个金币,他想用这些钱购买装备使自己的力量值尽量高。你能帮帮他吗?他会教你魔法Haunt(幽灵附体)作为回报的。
容易发现这是一个十分有(dú)趣(liú)的树上背包问题
高级装备和基本装备显然有树形的依赖关系
按照高级->高级->基本的方式进行建图,$(u,v)$ 的边权就是合成 $u$ 需要的 $v$ 个数
于是可以由此建出一个森林,因此最后dp的时候要加个虚拟结点啥的
设 $P[x],L[x],M[x]$ 分别表示物品 $x$ 的价值、购买上限和花费
对于基本装备(叶子结点),显然要进行L[x]=min(L[x],m/M[x])
的操作
设 $f[u][j][k]$ 表示 $u$ 装备所在子树,上传 $j$ 个 $u$ 装备用于给上层合成(也就是不计算这 $j$ 个的贡献),且所在子树花费的总金额为 $k$ 是能获得的最大价值
对于一个 $u$ ,它对父亲的贡献分为两方面
- 它所在的子树的贡献(价值),包括儿子的以及自己没上传的
- $u$ 上传的 $u$ 装备的个数,以用于父亲的合成
则我们首先要知道对于每个结点究竟合成几个
考虑枚举 $l$ ,表示 $u$ 要合成 $l$ 个(上传+自己私藏的)
然后我们就可以枚举上传几个以及花费多少了
如何知道对 $u$ 所在子树花费 $k$ 能获得的最大价值是多少呢
这个需要我们再单独做一个临时的dp
设 $g[i][j]$ 表示对于 $u$ 所在子树,只考虑 $u$ 的前 $i$ 个儿子所在子树,花费为 $j$ 时能获得的最大价值(这里不用记录 $u$ ,因为只是临时的dp)
则有
这里为什么是 $l\times w(u,v)$ ?因为已经枚举了当前要合成 $l$ 个
这里为什么max的第一个不是 $g[i-1][j+k]$ ?因为我们每个子树都要拿材料啊
所以这里只是代码这么写而已,
相当于 $g[i][j+k]=\max\{g[i-1][j]+f[v][l\times w(u,v)][k]\}$
这里为什么是 $j+k$ ?因为我喜欢刷表法,当然可以填表法
然后就可以愉快地推出 $f$ 的转移方程了
细节巨多,详见代码(这次有注释了qwq)
时间复杂度的宽松上界为 $O(100\times nm^2)$
实际剪枝+时限3.00s,可以444ms
跑过所有点(嘿嘿所以我是目前最优解
代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <iomanip>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define N (int)(55)
#define SZ (int)(2e3+15)
int n,m;
int P[N],M[N],L[N],tmp[SZ],h[SZ],f[N][105][SZ],g[SZ];
struct Edge
{
int u,v,w,next;
}e[N*N];
int pos=1,head[N],in[N];
void addEdge(int u,int v,int w)
{
e[++pos]={u,v,w,head[u]};
head[u]=pos;++in[v];
}
void DP(int u)
{
if(!head[u])
{
L[u]=min(L[u],m/M[u]);
for(int i=0; i<=L[u]; i++)
for(int j=i; j<=L[u]; j++)
f[u][i][j*M[u]]=(j-i)*P[u]; // j-i的会被私藏起来 qwq
return;
}
L[u]=INF;
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v; DP(v);
L[u]=min(L[u],L[v]/e[i].w); // 木桶能装多少水,和最短的那块板有关
M[u]+=e[i].w*M[v];
}
L[u]=min(L[u],m/M[u]); // 当然了,钱不够“木板”长也是没用的
for(int l=0; l<=L[u]; l++)
// 题解区都说是倒序枚举,表示不明白,正着枚举也可以过
// 他们的解释是,倒序枚举可以保证每一轮选的g[j]中所包含的决策至少选取了l*w个物品用于合成
// 可是如果没法选取l*w个物品的话,每一轮的g[j](也就是tmp[j])肯定是-INF了,应该不会影响答案的正确性
// 不太懂他们什么意思,如果我错了欢迎hack awa
{
memset(g,0xc0,sizeof(g));
g[0]=0; // 滚动数组优化
for(int i=head[u]; i; i=e[i].next)
{
int v=e[i].v,w=e[i].w;
for(int j=0; j<=m; j++)
tmp[j]=g[j],g[j]=-INF; // 注意转移方程不是从g[i-1][j+k]转移的
for(int j=0; j<=m; j++)
for(int k=0; tmp[j]>=0&&j+k<=m; k++) // 重要剪枝
g[j+k]=max(g[j+k],tmp[j]+f[v][l*w][k]);
}
for(int j=0; j<=l; j++)
for(int k=0; k<=m; k++)
f[u][j][k]=max(f[u][j][k],g[k]+(l-j)*P[u]);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
// freopen("check.in","r",stdin);
// freopen("check.out","w",stdout);
cin >> n >> m;
memset(f,0xc0,sizeof(f));
for(int i=1,l; i<=n; i++)
{
char ch;
cin >> P[i] >> ch;
if(ch=='A')
{
cin >> l;
for(int j=1,v,w; j<=l; j++)
cin >> v >> w,addEdge(i,v,w);
}
else cin >> M[i] >> L[i];
}
for(int u=1; u<=n; u++)
{
if(!in[u]) // 森林
{
DP(u);
for(int i=0; i<=m; i++)
tmp[i]=h[i],h[i]=0;
for(int i=0; i<=m; i++)
for(int j=0; i+j<=m; j++)
h[i+j]=max(h[i+j],tmp[i]+f[u][0][j]);
// 虚拟结点不用也无法合成物品,显然子结点上传只会浪费
}
}
int res=-INF;
for(int i=0; i<=m; i++)
res=max(res,h[i]);
cout << res << '\n';
return 0;
}
参考了很多的题解,就不一一列出来了 qwq