关于树形dp

关于树形dp

一点废话: 最近在学树形dp,感觉这玩意儿转移来转移去的真麻烦,果然和教练说的一样'不管什么东西挂到树上就难了“,当然这也怪我太菜,做了几道题感觉挺神奇,就随便写点东西刷下存在感(不会用markdown的我真是太菜了,感谢X老师教我\(\LaTeX\)),好了不baba了,上题目:

link(还是建议到codeforce上看)

Karen and Supermarket

题面描述

在回家的路上,凯伦决定到超市停下来买一些杂货。 她需要买很多东西,但因为她是学生,所以她的预算仍然很有限。

事实上,她最多只能花费 \(b\) 美元。

超市出售N种商品。第 \(i\) 件商品可以以 \(c_i\) 美元的价格购买。当然,每件商品只能买一次。

最近,超市一直在努力促销。凯伦作为一个忠实的客户,收到了 \(n\) 张优惠券。

如果 Karen 购买第 \(i\) 件商品,她可以使用第 \(i\) 张优惠券,将该商品的价格减少 \(d_i\) 美元。 当然,不买对应的商品,优惠券不能使用。

然而,对于优惠券有一个规则。对于所有 \(i\ge2\) ,为了使用 \(i\) 张优惠券,凯伦必须也使用第 \(x_i\) 张优惠券 (这可能意味着使用更多优惠券来满足需求。)

凯伦想知道。她能在不超过预算B的情况下购买的最大商品数量是多少?

数据范围: \(n\)\(b\) \((1<=n<=5000,1<=b<=10^9)\)

样例 #1

样例输入 #1

6 16
10 9
10 5 1
12 2 1
20 18 3
10 2 3
2 1 5

样例输出 #1

4

样例 #2

样例输入 #2

5 10
3 1
3 1 1
3 1 2
3 1 3
3 1 4

样例输出 #2

5

这道题有神奇的动转方程(语出X老师)首先分析题目:可怜的优惠券显然是一棵树,而商品却可以选择买与不买(抽象一下:买则说明当前节点是被搜索的对象),如果买,又可以选择优惠券的用与不用,那显然我们要把商品的各个状态挂在树上呐,于是,我们可以推出:

\[dp[u][i+j][0]=min(dp[u][i+j][0],dp[u][i][0]+dp[v][j][0]) \]

\[dp[u][i+j][1] = \min(dp[u][i+j][1],dp[u][i][1]+dp[v][j][1]) \]

\[dp[u][i+j][1] = \min(dp[u][i+j][1],dp[u][i][1]+dp[v][j][0]) \]

首先 \(dp[u][sum][1/0]\) 表示以 \(u\) 为节点,选 \(sum\) 件商品,是否使用券的最小价值( \(0\) 表示不用 \(1\) 表示用)其中 \(u\) 是父亲节点,\(v\) 自然是儿子节点,而 \(i+j\) 则是当前树节点的大小(即选 \(i+j\) 件商品)然后的状态转移就很简单了,这里给出代码:

void DP(int u){
    siz[u]=1;//维护当前节点的选择数量
    dp[u][0][0]=0;
    dp[u][1][0]=a[u];
    dp[u][1][1]=a[u]-d[u];//初始化应该不用多说
    for(int i=head[u];i;i=e[i].next){//存图方法当然不唯一
        int v=e[i].to;
        DP(v);
        for(int j=siz[u];j>=0;--j){//这里的倒序十分重要
            for(int k=0;k<=siz[v];++k){
                dp[u][j+k][0]=min(dp[u][j+k][0],dp[u][j][0]+dp[v][k][0]);
                dp[u][j+k][1]=min(dp[u][j+k][1],dp[u][j][1]+dp[v][k][1]);
                dp[u][j+k][1]=min(dp[u][j+k][1],dp[u][j][1]+dp[v][k][0]);
            }
        }
        siz[u]+=siz[v];//要记得与子节点相加
    }
}

值得注意的是:我们把 \(dp\) 的对象从价值转移到了商品数,这是因为限定价值的范围给到了恐怖的 \(10^9\) 显然没什么数组能存的下,所以转移是十分必要的。还有,对于 \(dp[i][0][1]\) 这种情况:\(i\) 节点所管辖的树中一件也不买,却用了券,这种不合理的情况,我们一定要将其 \(init\) 成一个极大值,而不是0!

  • 重要!重要!\(memset\) 的初值不能太大,因为动转涉及到了加和运算,太大的初值会爆,\(0x3f,0x7f\) 就刚刚好。

接下来就是找到 \(ans\) 了,找到 \(min(dp[1][i][0],dp[1][i][1])\) 小于 \(b\) 时的 \(i\) 就OK了

    for(int i=n;i>=1;--i){
        if(min(dp[1][i][1],dp[1][i][0])<=b){
            cout<<i<<endl;
            return 0;
        }
    }
    cout<<0<<endl;
    return 0;

这种枚举写法当然行,但是mikUUU老师却给了我更好的方法:二分!!!这样便完美的体现了代码的优雅与绚丽,不觉得这很酷吗?作为一名理工男我觉得这太酷了,很符合我对未来生活的想象,科技并带着趣味,代码如下:

    long long mid,R=n,L=1,ans;
    while(R>=L){
        mid=(R+L)/2;
        if(min(f[1][mid][1],f[1][mid][0])<=b){
            L=mid+1;
            ans=mid;
        }
        else R=mid-1;
    }

O ! K ! 完 !结 !

P.S.感觉这道题的确神奇,尤其是 \(dp[x][i+j][0/1]\) 的转移,X老师不谬哉!!!再有,这个 \(\LaTeX\)可累死我了

posted @ 2023-03-12 07:39  Melting_Pot  阅读(18)  评论(1编辑  收藏  举报