Loading

P6594 [YsOI2020] 换寝室 (树上差分+树形 dp)

P6594 [YsOI2020] 换寝室

upd 2024.11.15

树上差分+树形 dp

首先看到比较好处理的是老师的不满意值,一个边被割去的代价就是经过该边的老师数量。因为是离线的,所以转为树上差分预处理一下。

题目所求是最大值的最小值,这题里最大值越小越难达到,所以可以二分。

二分最大值后,就确定了所有连通块的上界。这题直接贪显然不现实(既要满足条件还要使代价最小),考虑 dp。

怎么设计状态?只考虑 \(u\) 子树以下,并且目前要确定包含 \(u\) 的连通块情况,还要描述最大值、最小值、最小代价。因为值域有效位置最多只有 \(n\) 个,所以只要记编号。那么就设 \(f_{u,i}\) 表示 \(u\) 所在连通块最小值编号为 \(i\) 的最小代价。

同一连通块最小值一定相同。据此区分断边不断边的情况。会不会出现断边后 \(u\) 的最小值不是 \(a_i\) 啊?但是不断边的代价一定更小,并且一定合法,会覆盖掉这个非法贡献,也就不用特殊考虑了。

现在问题是怎么预处理初始合法状态?就可以用二分的值对每个状态 check 一下,看看是否存在。

题意:给定一棵树,每条边有边权,割掉一些边,使得被割掉的边边权和不超过 \(k\) ,最小化剩余连通块点权极差的最大值。

看到最小化最大值,可以考虑二分

此时二分了 \(x\),那么每个连通块的极差都不能超过 \(x\)。考虑需要判断是否存在一个连通块的划分方式,使得满足条件并且代价不超过 \(k\),即最小化代价

考虑树形 dp。解决的问题是子树中满足条件的最小代价,然后可以加一个状态方便转移,考虑记录当前连通块的信息。设 \(f_{u,i}\) 表示划分完满足条件的 \(u\) 子树,包含 \(u\) 的连通块最小值为 \(a_i\) 的最小代价。转移枚举每个子树 \(v\) 是否与 \(u\) 在同一个连通块内。

\(f_{u,i}=\sum\min(f_{v,i},mn+val(u,v))\)

若最小值相同,那么无需断边;否则不在同一个连通块内,一定需要断边(不然肯定在一个连通块内)。这时候就有一个问题,有时候会出现不合法的情况,比如断边之后无法满足出现这样两个连通块。但是我们发现这种情况是不会更优的,也就是不会对答案产生贡献

考虑初始化,我们需要知道每个点的连通块中能够出现哪些最小值,限制是二分的 \(x\),只需要每个点跑一遍 dfs 即可

复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
#define pii std::pair<int, int>
#define fi first
#define se second
#define pb push_back

typedef long long i64;
const i64 iinf = 0x3f3f3f3f, linf = 0x3f3f3f3f3f3f3f3f;
const int N = 810;
int n, m, k, l = iinf, r, ans;
int dep[N], anc[N][11], sz[N], w[N], a[N];
std::vector<int> e[N];
void dfs(int u, int fa) {
	dep[u] = dep[fa] + 1;
	anc[u][0] = fa;
	for(int i = 1; i <= 10; i++) anc[u][i] = anc[anc[u][i - 1]][i - 1];
	for(auto v : e[u]) {
		if(v == fa) continue;
		dfs(v, u);
	}
}
int lca(int u, int v) {
	if(dep[u] < dep[v]) std::swap(u, v);
	for(int i = 10; i >= 0; i--) if(dep[anc[u][i]] >= dep[v]) u = anc[u][i];
	if(u == v) return u;
	for(int i = 10; i >= 0; i--) if(anc[u][i] != anc[v][i]) u = anc[u][i], v = anc[v][i];
	return anc[u][0];
}
void dfs2(int u, int fa) {
	for(auto v : e[u]) {
		if(v == fa) continue;
		dfs2(v, u);
		w[u] += w[v];
	}
} //lca+树上差分
int now;
int vis[N], f[N][N];
void find(int u, int fa, int rt) {
	vis[u] = 1;
	for(auto v : e[u]) {
		if(v == fa) continue;
		if(a[v] < a[rt] || a[v] - a[rt] > now) continue; 
		find(v, u, rt);
	}
} //包含 rt 的连通块中最小值是否可以是 a[rt]
void dfs3(int u, int fa) {
	int mn;
	for(auto v : e[u]) {
		if(v == fa) continue;
		mn = iinf;
		dfs3(v, u);
		for(int i = 1; i <= n; i++) mn = std::min(mn, f[v][i]);
		for(int i = 1; i <= n; i++) {
			f[u][i] += std::min(f[v][i], mn + w[v]);
		}
	}
}
bool check(int x) {
	now = x;
	for(int u = 1; u <= n; u++) {
		memset(vis, 0, sizeof(vis));
		find(u, 0, u);
		for(int i = 1; i <= n; i++) {
			if(vis[i]) f[i][u] = 0;
			else f[i][u] = iinf;
		}
	}
	dfs3(1, 0);
	for(int i = 1; i <= n; i++) {
		if(f[1][i] <= k) return true;
	}
	return false;
}
void Solve() {
	std::cin >> n >> m >> k;

	for(int i = 1; i <= n; i++) {
		std::cin >> a[i];
		l = std::min(l, a[i]), r = std::max(r, a[i]);
	}

	for(int i = 1; i < n; i++) {
		int u, v;
		std::cin >> u >> v;
		e[u].pb(v), e[v].pb(u);
	}
	dfs(1, 0);
	for(int i = 1; i <= m; i++) {
		int x, y;
		std::cin >> x >> y;
		int rt = lca(x, y);
		w[x]++, w[y]++, w[rt] -= 2;
	}
	dfs2(1, 0);
	r = r - l, l = 0;
	while(l <= r) {
		int mid = (l + r) >> 1;
		if(check(mid)) r = mid - 1, ans = mid;
		else l = mid + 1;
	}
	std::cout << ans << "\n";
}
int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    
	Solve();

	return 0;
}
posted @ 2024-04-13 12:03  Fire_Raku  阅读(31)  评论(0编辑  收藏  举报