关于树形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\)可累死我了