P6594 [YsOI2020] 换寝室 (树上差分+树形 dp)
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;
}