可怜与超市
这应该是HZOI学长出的题
星星星
感谢学长送来的质量题
当时刚开题就发现这是一个树形DP
赛后发现果然是一个树形DP
进入正题:
1.如何发现这是一个树形DP:
首先:
用劵的时候后面的劵对前面的劵有依赖性
这就可以看成子节点与父节点的关系
因此是一个树形
其次:
不必把每个劵都用掉
可以通过适当不用劵来减少因为依赖而额外购买的物品
这反而会得到更优的结果
2.如何设计DP?
阶段:
树状DP的一般方法就是去以整个树为阶段(即子问题)划分
因此,我们设计的状态转移数组的第1维就是:
以i为根节点的子树,也就是一个子问题
需要取用子节点最优值时,直接取用这个子节点即可
状态:
由于我们以最大购买物品数难以设计状态(以钱数为状态会超内存)
注:就是个普通背包
于是我们应该换个思路:
由于N的范围可以接受,因此我们设计状态应该是以一个不大于N的状态,
而money的最大值为1e9,我们更应该以一个整数的形式去存money
考虑状态反转,求最小值
自然想到把物品数当一维,money就老实当我们的储存数就行了
因此我们的第二维状态就是买到的物品数,当然,不计价格
而价格呢,就是我们要存储的,到最后,我们会依据价格同给定b的大小及购买货物的值确定答案
决策:
既然没有了价格限制,我们的决策自然变成了:
买j个,用劵或不用劵
而对于劵的依赖性:
由于父节点用了劵,子节点就可以选择用劵或不用劵,而如果父节点不用劵:那么子节点一定不能用劵
因此,我们要维护的有用劵和不用劵两个状态,这一维状态就是我们的决策
状态转移:
有了决策,就有了状态转移
而我们因为枚举不同的物品购买数,所以要借助一个辅助数组叫size;
size表示一个父节点当前所有可以购买的子节点数
接下来的任务就是遍历每一个子节点并访问子树
最终得到子问题的最优解
而由于0/1性质,我们需要倒序循环
对于每次决策:
决策维0:\(f[i][j+k][0]=min(f[i][j+k][0],f[i][j][0]+f[v][k][0])\)
秉承父不用劵子不用劵的原则还有尽可能省钱的原则,我们应当从以i为节点的子树中挑
j个,从v为节点的子树中挑k个最优答案
决策维1:\(f[i][j+k][0]=min(f[i][j+k][1],f[i][j][1]+min(f[y][k][0],f[y][k][1]))\)
i选了券决定f[i][j]带券,但是y就可带可不带选最小值了
于是这个程序写完2/3了
初始与目标
我们认为一开始只能买x一个所以size初值1
带券f[i][1][1]显然是c-d
不带券也显然是c
不买更显然直接0
其他初值正无穷(0x3f3f3f3f)
目标:
f[1][i][1]:1天生有券不用白不用,是唯一用了无副作用的券
查找:
对于f[1][i][1]如果花钱最小值小于b买得起就把ans更新成较大的i即可
最后就是前向星了
Code
点击查看代码
#include <bits/stdc++.h>//可怜与超市
#define sbcrs sbwqz//树形DP(依赖背包类树形)
using namespace std;//分类讨论
#define ll long long //开long long
const int o=5005;//数据规模
class supermarket{//定义数据类
public://公开访问:在其他位置可以使用
ll n,b,ans,h[o],cnt;//定义数据:n种类,b钱数,ans答案,h前向星用,cnt记边数
int size[o],f[o][o][2];//size存子节点个数,f状态转移
struct node{//定义数据节点
ll c;//c原价
ll d;//d优惠价
ll x;//x父节点编号
}a[o];//a存储每一个数据节点
struct tree{//建树/建图
int t;//终点
int n;//下一条边
}p[o*2];//
}w;//存储数据的类封装
namespace sbwqz{//定义函数封装
void add(ll s,ll t){//建边函数:s起点t终点
w.cnt++;//边数增加准备存边
w.p[w.cnt].t=t;//存终点
w.p[w.cnt].n=w.h[s];//存下一条边
w.h[s]=w.cnt;//保证链式
}
void pre(){//预处理
memset(w.f,0x3f,sizeof(w.f));//求最小值,初始化为较大数
}
void in(){//实现输入
scanf("%d%d",&w.n,&w.b);//输入n和b
scanf("%d%d",&w.a[1].c,&w.a[1].d);//对1号节点输入特殊处理
for(int i=2;i<=w.n;i++){//从2开始 进行正常输入
scanf("%d%d%d",&w.a[i].c,&w.a[i].d,&w.a[i].x);//分别输入c.d.x
add(w.a[i].x,i);//建边,a[i]是父节点,i子节点
}
}
void find(){//查找最优答案
for(int i=1;i<=w.n;i++){//从1到n遍历
if(w.f[1][i][1]<=w.b){//以1为根,对所有用劵的状态查找
w.ans=i;
}
}
}
void out(){//输出函数
cout<<w.ans;//输出答案
}
void work(int x){//求解函数 x为根节点
w.size[x]=1;//初始化,可访问点初始为1
w.f[x][1][1]=w.a[x].c-w.a[x].d;//如果只买x用劵的话显然
w.f[x][1][0]=w.a[x].c;//同理,不用劵也是显然对
w.f[x][0][0]=0;//直接不买花销自然为0
for(int i=w.h[x];i;i=w.p[i].n){//遍历所有边
int y=w.p[i].t;//找到终点 (子节点)
work(y);//对子节点进行求解
for(int j=w.size[x];j>=0;j--){//0/1背包DP倒序循环
for(int k=w.size[y];k>=0;k--){//同理
w.f[x][j+k][1]=min(w.f[x][j+k][1],w.f[x][j][1]+min(w.f[y][k][0],w.f[y][k][1]));//用劵的最小值
w.f[x][j+k][0]=min(w.f[x][j+k][0],w.f[x][j][0]+w.f[y][k][0]);//不用劵的最小值
}
}
w.size[x]+=w.size[y];//增大x规模
}
}
}
using namespace sbcrs;//调用封装好的函数准备实现功能
int main(){
freopen("supermarket.in","r",stdin);//文件输入
freopen("supermarket.out","w",stdout);//文件输出
pre();//预处理
in();//输入
work(1);//求解
find();//查找最优解
out();//输出
return 0;//结束
}
完结撒花!
RP++!