Min-Max Tree

Min-Max Tree

1. 问题重述

给定一棵 n 个节点的无根树,每个节点有权值 a[i]。
删除若干条边将树分成若干个连通分量,每个分量的价值定义为 分量内最大权值 − 最小权值。
求所有分量价值之和的最大值。


2. 为什么贪心(按边权排序合并)不正确?

直观上,如果我们把一条边两端的连通块合并,新连通块的价值并不是原来两个价值之和,而是 max(max1,max2) − min(min1,min2),这个值可能远大于或远小于原来两个块的价值之和。
例如:两个块的价值分别为 99 和 88,合并后可能仅为 99,相加反而更大。
因此,本题不能简单地用最大生成树类贪心,必须用树形 DP 枚举所有分割方案。


3. 树形 DP 状态设计

以 1 为根,对每个节点 u 定义三个 DP 值:

状态 含义
dp[u][0] u 的子树完全闭合(即 u 与父节点的边被切断)时的最大总价值。
dp[u][1] u 与父节点保持连通,且当前开放分量只确定了最小值(最大值未知)时,子树贡献的最优值。存储 (内部闭合和) − (当前最小值)。
dp[u][2] u 与父节点保持连通,且当前开放分量只确定了最大值(最小值未知)时,子树贡献的最优值。存储 (内部闭合和) + (当前最大值)。

注意:dp[u][1] 和 dp[u][2] 并不直接是价值,而是“带符号”的辅助值,用于在合并两个开放分量时直接相加得到闭合价值。


4. 临时状态和转移

在处理节点 u 时,我们维护四个临时变量 cur0, cur1, cur2, cur3,表示已经处理了部分子节点后,u 所在开放分量的四种状态:

变量 极值情况 存储内容
cur0 未确定任何极值 内部闭合和(初始为 0)
cur1 只确定了最小值 内部闭合和 − 当前最小值
cur2 只确定了最大值 内部闭合和 + 当前最大值
cur3 同时确定了最小值和最大值 内部闭合和 + (最大值 − 最小值) = 闭合总价值

初始化(未处理任何子节点,只有 u 自己):

  • `cur0 = 0
  • `cur1 = -a[u]
  • `cur2 = a[u]
  • `cur3 = -INF

处理一个子节点 v

子节点 v 已经通过 DFS 计算出了 dp[v][0], dp[v][1], dp[v][2]。
我们考虑边 (u, v) 的两种决策:

① 切断边
v 的子树完全闭合,其价值为 dp[v][0],直接加到 cur 所有状态上:

  • `g0 = cur0 + dp[v][0]
  • `g1 = cur1 + dp[v][0]
  • `g2 = cur2 + dp[v][0]
  • `g3 = cur3 + dp[v][0]

② 保留边,合并开放分量
此时 v 必须处于开放状态,只能使用 dp[v][1] 或 dp[v][2]。
根据当前 cur 的极值状态和 v 的极值状态,有四种合并方式:

当前 cur 状态 v 的状态 合并后的状态 更新方式
cur0 dp[v][1] cur1 g1 = max(g1, cur0 + dp[v][1])
cur0 dp[v][2] cur2 g2 = max(g2, cur0 + dp[v][2])
cur1 dp[v][2] cur3 g3 = max(g3, cur1 + dp[v][2])
cur2 dp[v][1] cur3 g3 = max(g3, cur2 + dp[v][1])

为什么没有 cur1 + dp[v][1] 或 cur2 + dp[v][2]?
因为两个都只确定同一类极值,合并后仍需比较大小才能确定新的极值,而我们的表示法无法通过简单加法完成比较,并且这样的合并不会优于“先切断其中一个”的方案,因此最优解不会采用,故不纳入转移。

最后,将 g0,g1,g2,g3 赋值给 cur0,cur1,cur2,cur3,完成一个子节点的合并。


处理完所有子节点后

当所有子节点处理完毕,cur0,cur1,cur2,cur3 代表了 u 最终的状态。

  • dp[u][0] = max(cur0, cur3):闭合 u 所在分量,要么未确定极值(极差为0),要么极值已确定(已含极差)。
  • dp[u][1] = cur1
  • dp[u][2] = cur2

5. 为什么 cur3 不参与后续合并?

cur3 已经同时确定了最小值和最大值,它存储的值就是“闭合总价值”。如果继续向上合并,极值可能会被新的节点改变,而我们的状态机没有提供从 cur3 出发的合并规则。
但这不会丢失最优解,因为如果最优解中需要继续合并,那么在合并之前,开放分量一定还处于 cur1 或 cur2 状态(即只确定了一个极值),这样才能与新的极值正确合并。
cur3 的作用是作为闭合候选,最终在 dp[u][0] 中被考虑。


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 10;
const ll INF = 1e17;

ll dp[N][3];//0为子树根父节点切断,完全独立,此时没有极值。1表示存在极小值,2表示存在极大值
vector<ll> a(N);
void dfs(const vector<vector<int>>& grid, int u, int pa) {
    ll cur0 = 0, cur1 = -a[u], cur2 = a[u], cur3 = -INF;
    //cur0 已经定型,没有极值
    //cur1 确定minv, cur2 确定maxv
    //cur3 两个极值都确定的定型情况,也就是产生新的闭合的价值
    for(auto&v : grid[u]) {
        if(v == pa) continue;
        dfs(grid, v, u);
        
        ll g0 = cur0 + dp[v][0], g1 = cur1 + dp[v][0], g2 = cur2 + dp[v][0], g3 = cur3 + dp[v][0];
        //初始情况为与父节点切断
        
        g1 = max(g1, cur0 + dp[v][1]);
        g2 = max(g2, cur0 + dp[v][2]);
        g3 = max(g3, cur1 + dp[v][2]); g3 = max(g3, cur2 + dp[v][1]);
        
        cur0 = g0, cur1 = g1, cur2 = g2, cur3 = g3;
    }
    dp[u][0] = max(cur0, cur3);
    dp[u][1] = cur1, dp[u][2] = cur2;
}

void solve() {
    int n; cin >> n;
    vector<vector<int>> grid(n + 1);
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i < n; i ++) {
        int u, v; cin >> v >> u;
        grid[u].push_back(v);
        grid[v].push_back(u);
    }
    
    for(int i = 1; i <= n; i ++) {
        dp[i][0] = 0;
        dp[i][1] = -a[i];
        dp[i][2] = a[i];
    }
    dfs(grid, 1, 0);
    cout << dp[1][0] << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    int _=1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-09-04 15:00  thenrify  阅读(37)  评论(0)    收藏  举报