可怜与超市

这应该是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++!

posted @ 2022-02-10 19:54  2K22  阅读(85)  评论(0)    收藏  举报