BZOJ4182 - Shopping

Update on 2022.7.13 \text{Update on 2022.7.13} Update on 2022.7.13

一些闲话:依稀记得之前做过一道类似的题,但是怎么都找不到了 🥲。

Update \text{Update} Update:找到那篇博客了(就是这道题),竟然还是 2 2 2 年前写的。那时候还在 c s d n \rm csdn csdn 上呢。

首先题目限制实际上可以转化为选一个连通块,于是有一个 O ( n m 2 ) \mathcal{O}(nm^2) O(nm2) d p \tt dp dp:令 d p ( i , j ) dp(i,j) dp(i,j) 为以 i i i 为子树(必选 i i i),花费为 j j j 的最大价值。每个连通块都在它深度最浅的点上计数了。可以发现,背包的瓶颈在于两个背包的合并是 O ( m 2 ) \mathcal O(m^2) O(m2) 的。

插入一个元素可以用单调队列做到 O ( m ) \mathcal O(m) O(m),所以我们尝试用 dfs 序考虑这个问题。但是用 dfs 序也就意味着只能考虑 dfs 序为 [ 1 , i ] [1,i] [1,i] [ i , n ] [i,n] [i,n] 的一段区间,不妨就考虑 [ 1 , i ] [1,i] [1,i] 区间,我们无法求出某一个子树的贡献,只能算出根节点(也就是 dfs 序为 1 1 1)的贡献,且时间复杂度为 O ( n m ) \mathcal O(nm) O(nm).

于是可以想到点分治。复杂度降到 O ( n m log ⁡ n ) \mathcal O(nm\log n) O(nmlogn).


为什么用点分治?因为它快啊。

我们发现,按题目要求买出的玩意一定是联通的。虽然点分治会是树的形态发生改变,但等到改变时其实已经不需要root了。(将包含root的处理完了)而且其可以保证遍历到所有点。

最后,我们可以用多重背包的二进制优化来做dp。

至于为什么d数组输入时要减去1,是因为我们保证要买一个,就直接算了。

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;

const int N = 502, M = 4002;
int tot, dp[N][M], ans, siz[N], maxx[N], root, T, n, m, w[N], c[N], d[N], cnt, head[N], nxt[N << 1], dot[N << 1];
bool vis[N];

void addEdge(const int u, const int v) {
	dot[++ cnt] = v;
	nxt[cnt] = head[u];
	head[u] = cnt;
}

void init() {
	cnt = 0;
	ans = 0;
	maxx[0] = n;
	tot = n;
	root = 0;
	memset(vis, 0, sizeof vis);
	memset(head, 0, sizeof head);
}

void getRoot(const int u, const int ba) {
	maxx[u] = 0, siz[u] = 1;
	for(int i = head[u]; i; i = nxt[i]) {
		int v = dot[i];
		if(v == ba || vis[v]) continue;
		getRoot(v, u);
		siz[u] += siz[v];
		maxx[u] = max(maxx[u], siz[v]); 
	}
	maxx[u] = max(maxx[u], tot - siz[u]);
	if(maxx[root] > maxx[u]) root = u;
}

void cal(const int u, const int ba, const int lim) {
	if(lim <= 0) return;
	int i, j;
	for(i = 1, j = d[u]; i < j; j -= i, i <<= 1)
		for(int k = lim; k >= i * c[u]; -- k)
			dp[u][k] = max(dp[u][k], dp[u][k - i * c[u]] + i * w[u]);
	for(int k = lim; k >= j * c[u]; -- k)
		dp[u][k] = max(dp[u][k], dp[u][k - j * c[u]] + j * w[u]);
	for(i = head[u]; i; i = nxt[i]) {
		int v = dot[i];
		if(v == ba || vis[v]) continue;
		for(j = 0; j <= lim - c[v]; ++ j) dp[v][j] = dp[u][j] + w[v];
		cal(v, u, lim - c[v]);
		for(j = 0; j <= lim - c[v]; ++ j) dp[u][j + c[v]] = max(dp[u][j + c[v]], dp[v][j]);
	}
}

void divide(const int u) {
	vis[u] = 1;
	for(int i = 0; i <= m - c[u]; ++ i) dp[u][i] = w[u];
	cal(u, 0, m - c[u]);
	for(int i = 0; i <= m - c[u]; ++ i) ans = max(ans, dp[u][i]);
	for(int i = head[u]; i; i = nxt[i]) {
		int v = dot[i];
		if(vis[v]) continue;
		root = 0;
		tot = siz[v];
		getRoot(v, 0);
		divide(root);
	}
}

int main() {
	int a, b;
	scanf("%d", &T);
	while(T --) {
		scanf("%d %d", &n, &m);
		init();
		for(int i = 1; i <= n; ++ i) scanf("%d", &w[i]);
		for(int i = 1; i <= n; ++ i) scanf("%d", &c[i]);
		for(int i = 1; i <= n; ++ i) scanf("%d", &d[i]), -- d[i];
		for(int i = 1; i < n; ++ i) {
			scanf("%d %d", &a, &b);
			addEdge(a, b); addEdge(b, a);
		}
		getRoot(1, 0);
		divide(root);
		printf("%d\n", ans);
	}
	return 0;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值