P6326 - Shopping 题解

一个很不寻常的题啊。

首先容易发现题目说的就是所有至少买了一个的点必须构成连通块。考虑在树上跑背包,那必须是有根的。随便设 \(1\)​ 为根,然后规定若 \(x\)​ 选了,则 \(x\)​ 的所有祖先都必须选,这样随后调用 \(1\)​ 的 DP 数组即可得到必须选 \(1\) 时的答案。设 \(dp_{i,j}\) 为子树 \(i\) 内花了不超过 \(j\) 元钱得到的最大收益,转移的时候将若干儿子树的 DP 数组合并起来,然后将 \(i\) 加入。这样你会发现,根本不需要令每个点为根跑一遍,因为一个连通块在有根树上的最浅点是唯一的,直接枚举所有点 \(x\) 作为最浅点,答案就是它的 \(dp_{x,m}\),max 起来即可。

这个树形背包跟普通的复杂度可以分析到 \(\mathrm O(nm)\) 的不同,这玩意是基于值域的,背包大小并没有和 size 取 min,每次合并是严格 \(\mathrm O\!\left(m^2\right)\) 的,总复杂度是 \(\mathrm O\!\left(nm^2\right)\),接受不了。注意到将 \(i\) 加入这个操作实际上是多重背包加入一个元素,可以用单调队列优化到 \(\mathrm O(m)\),然并卵!毕竟复杂度瓶颈在于背包的「合并」。

我们现在认为必须要根除背包的合并操作。此时引出一个科技:树上依赖性背包。问题就是说在有根树上,\(x\)​ 选了则必须选所有祖先的一个背包,无论是 01 还是完全还是多重。这玩意除了上述的暴力合并背包的 DP 做法,还有个比较妙的不带合并操作的 DP 做法。
我们考虑像一个新的 DP 题一样从头开始考虑:一开始的局面是 \(1\sim n\)​ 所有点未决策,现在考虑决策 \(1\)​。如果不选 \(1\)​ 那就直接结束走人吧!否则剩下 \(2\sim n\)​ 需要决策,做完之后返回一个背包再将 \(1\)​ 插入。现在问题转化为 \(2\sim n\)​。注意到这里的精髓:我们并没有将 \(2\sim n\)​ 拆成若干儿子树再合并,而是直接当做一个整体!毕竟它本来就是一个整体,为啥要拆开再合并呢?(普通树形 DP 这么做是因为不会有「合并复杂度大,插入复杂度小」这样奇怪的事情发生,在这个前提下每个子树作为子问题显然是好理解的)
现在假设 \(2\)​ 是 \(1\)​ 的儿子,对 \(2\)​ 进行决策。如果选的话那就 \(3\sim n\)​ 走起;否则剩下 \(\{2\sim n\}-\mathrm{subt}(2)\)​。对后者下一步应该选 \(1\)​ 的下一个儿子对吧这很自然。对前者呢?如果也选 \(1\)​ 的下一个儿子,那么得到的状态就是 \(V\)​​ 去掉一个儿子树再去掉一个儿子树的根???这状态就显得支离破碎,没法封闭。接下来官方给出的下一步决策的方案是这个算法的精髓:选 \(3\sim n\)​ 中 dfn 最小的。就假设 \(dfn_i=i\)​ 吧,如果选的话转化到 \(4\sim n\)​,否则去除 \(\mathrm{subt}(3)\)​,而注意到 \(3\)​ 子树里的点都大于 \(3\)​,而且是连续的,所以是 \(3\sim n\)​ 的前缀,去掉之后得到 \(1\sim n\)​ 的某个后缀。这样我们只要对每个状态都对 dfn 最小的点进行决策,转移到的状态就可以封闭在「dfn 后缀」这仅仅 \(\mathrm O(n)\)​​ 个状态里,而且每次转移都是加入一个点的操作。

通过上述科技,我们可以用单调队列在 \(\mathrm O(nm)\) 时间内计算出连通块必须包含 \(1\) 的答案。但这样一个坏处是不能顺便得到连通块最浅节点为其它点的答案了。那难道要对每个点都跑一遍吗?这样是 \(\mathrm O\!\left(n^2m\right)\),虽然比原来的做法优,但还是过不去。不难发现,考虑过包含 \(1\) 的情况,剩下不包含 \(1\) 的情况,这样必须在 \(1\) 的若干个儿子树的其中之一,就转化为了子问题,递归下去。可以用点分治来限制层数,达到 \(\mathrm O(nm\log n)\)。这里点分治不再是统计路径,而是统计连通块,也很自然对吧?

code(好写!)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=510,M=4010;
int n,m;
int v[N],w[N],lim[N];
vector<int> nei[N];
bool vis[N];
int dfn[N],mxdfn[N],nowdfn,mng[N];
void dfs(int x,int fa=0){
	mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];if(y==fa||vis[y])continue;
		dfs(y,x);
		mxdfn[x]=mxdfn[y];
	}
}
int dp[N][M];
int q[M],head,tail;
int ans;
int sz[N],mxsz[N];
bool cmp(int x,int y){return mxsz[x]<mxsz[y];}
int gtrt(int x=1,int tot=n,int fa=0){
	sz[x]=1,mxsz[x]=0;
	int rt=0;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];if(y==fa||vis[y])continue;
		rt=min(rt,gtrt(y,tot,x),cmp);
		sz[x]+=sz[y],mxsz[x]=max(mxsz[x],sz[y]);
	}
	mxsz[x]=max(mxsz[x],tot-sz[x]);
	return min(rt,x,cmp);
}
void cdq(int x){
	nowdfn=0,dfs(x);
	memset(dp[nowdfn+1],0,sizeof(dp[0]));
	for(int i=nowdfn;i;i--){
		int u=mng[i];
		for(int j=0;j<=m;j++)dp[i][j]=dp[mxdfn[u]+1][j];
		for(int j=0;j<w[u]&&j<=m;j++){
			head=tail=0;
			for(int k=j;k<=m;k+=w[u]){
				if(k>=w[u]){
					while(head<tail&&dp[i+1][q[tail-1]]-q[tail-1]/w[u]*v[u]<=dp[i+1][k-w[u]]-(k-w[u])/w[u]*v[u])tail--;
					q[tail++]=k-w[u];
				}
				while(head<tail&&(k-q[head])/w[u]>lim[u])head++;
				if(head<tail)dp[i][k]=max(dp[i][k],dp[i+1][q[head]]+(k-q[head])/w[u]*v[u]);
			}
		}
	}
	ans=max(ans,dp[1][m]);
	vis[x]=true;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];if(vis[y])continue;
		cdq(gtrt(y,sz[y]));
	}
}
void mian(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)nei[i].clear();
	for(int i=1;i<=n;i++)scanf("%lld",v+i);
	for(int i=1;i<=n;i++)scanf("%lld",w+i);
	for(int i=1;i<=n;i++)scanf("%lld",lim+i);
	for(int i=1;i<n;i++){
		int x,y;
		scanf("%lld%lld",&x,&y);
		nei[x].pb(y),nei[y].pb(x);
	}
	memset(vis,0,sizeof(vis));
	ans=0,cdq(gtrt());
	cout<<ans<<"\n";
}
signed main(){mxsz[0]=inf;
	int t;
	cin>>t;
	while(t--)mian();
	return 0;
}
posted @ 2021-10-04 23:12  ycx060617  阅读(75)  评论(0编辑  收藏  举报