洛谷P4037 [JSOI2008]魔兽地图 题解

洛谷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) (u,v) 的边权就是合成 u u u 需要的 v v v 个数

于是可以由此建出一个森林,因此最后dp的时候要加个虚拟结点啥的

P [ x ] , L [ x ] , M [ x ] P[x],L[x],M[x] P[x],L[x],M[x] 分别表示物品 x x x 的价值、购买上限和花费

对于基本装备(叶子结点),显然要进行L[x]=min(L[x],m/M[x])的操作

f [ u ] [ j ] [ k ] f[u][j][k] f[u][j][k] 表示 u u u 装备所在子树,上传 j j j u u u 装备用于给上层合成(也就是不计算这 j j j 个的贡献),且所在子树花费的总金额为 k k k 是能获得的最大价值

对于一个 u u u ,它对父亲的贡献分为两方面

  • 它所在的子树的贡献(价值),包括儿子的以及自己没上传的
  • u u u 上传的 u u u 装备的个数,以用于父亲的合成

则我们首先要知道对于每个结点究竟合成几个

考虑枚举 l l l ,表示 u u u 要合成 l l l 个(上传+自己私藏的)

然后我们就可以枚举上传几个以及花费多少了

如何知道对 u u u 所在子树花费 k k k 能获得的最大价值是多少呢

这个需要我们再单独做一个临时的dp

g [ i ] [ j ] g[i][j] g[i][j] 表示对于 u u u 所在子树,只考虑 u u u 的前 i i i 个儿子所在子树,花费为 j j j 时能获得的最大价值(这里不用记录 u u u ,因为只是临时的dp)

则有
g [ i ] [ j + k ] = max ⁡ ( g [ i ] [ j + k ] , g [ i − 1 ] [ j ] + f [ v ] [ l × w ( u , v ) ] [ k ] ) g[i][j+k]=\max(g[i][j+k],g[i-1][j]+f[v][l\times w(u,v)][k]) g[i][j+k]=max(g[i][j+k],g[i1][j]+f[v][l×w(u,v)][k])

  • 这里为什么是 l × w ( u , v ) l\times w(u,v) l×w(u,v) ?因为已经枚举了当前要合成 l l l

  • 这里为什么max的第一个不是 g [ i − 1 ] [ j + k ] g[i-1][j+k] g[i1][j+k] ?因为我们每个子树都要拿材料啊

    所以这里只是代码这么写而已,

    相当于 g [ i ] [ j + k ] = max ⁡ { g [ i − 1 ] [ j ] + f [ v ] [ l × w ( u , v ) ] [ k ] } g[i][j+k]=\max\{g[i-1][j]+f[v][l\times w(u,v)][k]\} g[i][j+k]=max{g[i1][j]+f[v][l×w(u,v)][k]}

  • 这里为什么是 j + k j+k j+k ?因为我喜欢刷表法,当然可以填表法

然后就可以愉快地推出 f f f 的转移方程了
f [ u ] [ j ] [ k ] = max ⁡ ( f [ u ] [ j ] [ k ] , g [ k ] + ( l − j ) × P [ u ] ) f[u][j][k]=\max(f[u][j][k],g[k]+(l-j) \times P[u]) f[u][j][k]=max(f[u][j][k],g[k]+(lj)×P[u])
细节巨多,详见代码(这次有注释了qwq

时间复杂度的宽松上界为 O ( 100 × n m 2 ) O(100\times nm^2) O(100×nm2)

实际剪枝+时限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

转载请说明出处

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值