题目来源:Codeforces,Codeforces Round #664 (Div. 1),CF1394,CF1394D,Boboniu and Jianghu。出题人:高爸,高麟翔,CF id:ZZZZZZZZZZZZZZZZZZ。
题目大意
波波牛每天都在山上练习功夫。
波波牛设计了一张 (n) 座山的地图。他用 (n-1) 条边将山相连,保证整张图联通,也就是说,这张图形成了一个树的结构。
对于第 (i) 座山,波波牛评估了在这座山上练功夫的疲惫值 (t_i)。同时,他还计算出了每座山的高度 (h_i)。
我们定义一条路径,是一个山的序列 (M),满足 (forall 1leq i < |M|),(M_i) 和 (M_{i+1}) 之间都有边相连。波波牛认为一条路径是一个挑战,当且仅当 (forall 1leq i < |M|:h_{M_i}leq h_{M_{i + 1}})。
波波牛会将所有 (n-1) 条边划分成若干个挑战,使得每条边都恰好出现在一个挑战里。
我们定义一个挑战的疲惫值为该挑战里所有节点的疲惫值之和,即:(sum_{i=1}^{|M|}h_{M_i})。
请你划分这些挑战,使得所有挑战的疲惫值最小。你只需要输出这个最小的疲惫值。
数据范围:(1leq nleq 2cdot 10^5)。
本题题解
考虑 (h) 单调这个要求。我们可以理解为给边定向,使得同一条路径上的边方向相同(也就是从某个端点可以依次按方向经过所有边到达另一个端点)。不妨约定,对于树上的边 ((x,y)),如果方向是 (x ightarrow y),则 (h_xleq h_y)。也就是说,当 (h_x eq h_y) 时,边的方向是已经确定的。我们要做的是给所有 (h_x=h_y) 的边定一个方向。
定好方向后,如何计算答案?首先,一种一定合法的方案是,每条边单独作为一条路径。此时的代价和是 (sum_{i = 1} ^ {n} ext{deg}(i) imes t_i),其中 ( ext{deg}(i)) 表示树上节点 (i) 的度数。但这显然不一定是最优方案。我们来做一些调整使它变得更优。选取一个节点 (u),一条以 (u) 为终点的路径和一条以 (u) 为起点的路径拼接起来,则总代价会减少 (t_u)。我们希望通过拼接一些路径,使得总代价的“减少值”最大。
综上所述,我们的工作有两个:
- 给边定向。
- 将定向好的边拼接。
这两个工作可以通过一次树形 DP 同时完成。
先任意钦定一个根节点。设 (dp[u][0]) 表示节点 (u) 和父亲 (fa) 之间的边方向是 (u ightarrow fa),只考虑 (u) 子树内的节点,它们最多能使总代价减少多少;(dp[u][1]) 表示方向是 (fa ightarrow 1) 时,最多减少多少。特别地,对于根节点而言,它没有父亲,所以 (dp[ ext{root}][0] = dp[ ext{root}][1])。值得注意的是,DP 时计算的并不是答案,(sum_{i = 1} ^ {n} ext{deg}(i) imes t_i-dp[ ext{root}][0]) 才是答案。所以我们要使 DP 值尽可能大。
转移,考虑 (u) 的儿子 (v)。可以分成三类:
- (h_v<h_u)
- (h_v>h_u)
- (h_v = h_u)
设三类儿子的数量分别为 (c_1,c_2,c_3)。为了方便讨论,不妨假设 (c_1leq c_2)((c_1>c_2) 时做法类似)。
对于前两类,方向是确定的,它们的 DP 值(第 1 类 (dp[v][0]),第 2 类 (dp[v][1]))可以直接加到 (dp[u]) 里。并且可以先将它们尽可能多地两两配对,每成功匹配一对就使 (dp[u]) 增大 (t_u)。于是我们会匹配掉 (c_1) 对,并且剩下 (c_2-c_1) 个落单的第 2 类儿子。
接下来,问题的关键是第 3 类儿子如何定向。考虑有 (i) 个第 3 类儿子方向定为 (v ightarrow u),其他 (c_3-i) 个定为 (u ightarrow v)。那么具体选哪 (i) 个呢?发现两种方向对 (dp[u]) 的贡献分别为 (dp[v][0]) 和 (dp[v][1]),所以我们可以将第三类儿子按 (dp[v][0]-dp[v][1]) 的值从大到小排序。然后取前 (i) 大的。你可以把这个策略理解为:先假设全取 (dp[v][1]),然后贪心挑 (i) 个换成 (dp[v][0])。
定向后,这些儿子要进行匹配。匹配有两种:和落单的第 2 类儿子匹配,或者两种方向的第 3 类儿子内部匹配。枚举了 (i),很容易 (O(1)) 计算出匹配的数量。
最后总结一下,(dp[u]) 的值来自两个部分,分别是 (dp[v]),以及 (t_u imes ext{新增的匹配数})。对前两类儿子来说,(dp[v]) 的贡献是确定的,对第 3 类儿子,我们枚举有 (i) 个 (dp[v][0]),然后贪心取 (dp[v][0] - dp[v][1]) 前 (i) 大的即可。新增的匹配数,又可以分为四个部分:(1) 前两类儿子,尽可能多匹配;(2) 前两类儿子匹配完多出来的部分,和第 3 类儿子匹配;(3) 第 3 类儿子内部匹配;(4) 如果最后多 (1) 条边,可能可以和 ((u,fa)) 匹配(视 ((u,fa)) 的方向而定)。
时间复杂度 (O(nlog n)),其中 (log n) 来自排序。
参考代码
实际提交时,建议加上读入优化,详见本博客公告。
// problem: CF1394D
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 2e5;
int n, tired[MAXN + 5], hight[MAXN + 5];
ll dp[MAXN + 5][2];
vector<int> G[MAXN + 5];
vector<pair<ll, int> > eq[MAXN + 5];
void dfs(int u, int fa) {
ll sum_le = 0, sum_gr = 0;
int cnt_le = 0, cnt_gr = 0;
for(int i = 0; i < SZ(G[u]); ++i) {
int v = G[u][i];
if(v == fa) continue;
dfs(v, u);
if(hight[v] < hight[u]) {
cnt_le++;
sum_le += dp[v][0];
}
else if(hight[v] > hight[u]) {
cnt_gr++;
sum_gr += dp[v][1];
}
else {
eq[u].pb(mk(dp[v][0] - dp[v][1], v));
}
}
if(cnt_le <= cnt_gr) {
ll sum = sum_le + sum_gr + (ll)cnt_le * tired[u];
int rest = cnt_gr - cnt_le;
sort(eq[u].begin(), eq[u].end());
reverse(eq[u].begin(), eq[u].end()); // 从大到小
ll eqcur = 0;
for(int i = 0; i < SZ(eq[u]); ++i) eqcur += dp[eq[u][i].se][1];
ckmax(dp[u][0], sum + eqcur);
ckmax(dp[u][1], sum + eqcur + (u != 1 && rest + SZ(eq[u]) > 0) * tired[u]);
for(int i = 1; i <= SZ(eq[u]); ++i) {
// eq 里选几个 0
eqcur += eq[u][i - 1].fi;
int mat = 0, have_0 = 0, have_1 = 0;
if(i <= rest) {
mat = i;
have_1 = (i < rest || i < SZ(eq[u]));
} else {
mat = rest + min(i - rest, SZ(eq[u]) - i);
have_0 = (i - rest > SZ(eq[u]) - i);
have_1 = (i - rest < SZ(eq[u]) - i);
}
if(u == 1) have_0 = have_1 = 0;
ckmax(dp[u][0], sum + eqcur + (ll)(mat + have_0) * tired[u]);
ckmax(dp[u][1], sum + eqcur + (ll)(mat + have_1) * tired[u]);
}
} else {
ll sum = sum_le + sum_gr + (ll)cnt_gr * tired[u];
int rest = cnt_le - cnt_gr;
sort(eq[u].begin(), eq[u].end());
ll eqcur = 0;
for(int i = 0; i < SZ(eq[u]); ++i) eqcur += dp[eq[u][i].se][0];
ckmax(dp[u][0], sum + eqcur + (u != 1 && rest + SZ(eq[u]) > 0) * tired[u]);
ckmax(dp[u][1], sum + eqcur);
for(int i = 1; i <= SZ(eq[u]); ++i) {
// eq 里选几个 1
eqcur -= eq[u][i - 1].fi;
int mat = 0, have_0 = 0, have_1 = 0;
if(i <= rest) {
mat = i;
have_0 = (i < rest || i < SZ(eq[u]));
} else {
mat = rest + min(i - rest, SZ(eq[u]) - i);
have_0 = (i - rest < SZ(eq[u]) - i);
have_1 = (i - rest > SZ(eq[u]) - i);
}
if(u == 1) have_0 = have_1 = 0;
ckmax(dp[u][0], sum + eqcur + (ll)(mat + have_0) * tired[u]);
ckmax(dp[u][1], sum + eqcur + (ll)(mat + have_1) * tired[u]);
}
}
}
int main() {
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> tired[i];
for(int i = 1; i <= n; ++i)
cin >> hight[i];
for(int i = 1; i < n; ++i) {
int u, v; cin >> u >> v;
G[u].pb(v); G[v].pb(u);
}
dfs(1, 0);
ll ans = 0;
for(int i = 1; i <= n; ++i)
ans += (ll)tired[i] * SZ(G[i]);
// cerr << ans << endl;
assert(dp[1][0] == dp[1][1]);
ans -= dp[1][0];
cout << ans << endl;
// int i, j; while(1) { cin >> i >> j; cout << dp[i][j] << endl; }
return 0;
}