#include using namespace std; const int NEG = -1e9; struct Input { int n; vector> g; vector a; }; Input read_input() { Input in; cin >> in.n; in.g.assign(in.n, {}); for (int i = 0; i < in.n - 1; i++) { int u, v; cin >> u >> v; --u, --v; in.g[u].push_back(v); in.g[v].push_back(u); } in.a.resize(in.n); for (int& x : in.a) cin >> x; return in; } struct DSU { vector p, sz; DSU(int n = 0) : p(n, -1), sz(n, 1) {} int root(int x) { return p[x] < 0 ? x : p[x] = root(p[x]); } bool unite(int a, int b) { a = root(a); b = root(b); if (a == b) return false; if (sz[a] < sz[b]) swap(a, b); p[b] = a; sz[a] += sz[b]; return true; } }; bool legal(int x, int y) { return (x & 1) == (y & 1); } bool is02(int x, int y) { return (x == 0 && y == 2) || (x == 2 && y == 0); } // ============================================================ // 動的に木を縮約するための雑なヘルパー // ============================================================ struct DynTree { int n; vector> adj; vector val; vector alive; DynTree(const Input& in) : n(in.n), adj(n), val(in.a), alive(n, 1) { for (int v = 0; v < n; v++) { for (int u : in.g[v]) adj[v].insert(u); } } bool connected(int u, int v) const { return 0 <= u && u < n && 0 <= v && v < n && alive[u] && alive[v] && adj[u].count(v); } int merge_edge(int u, int v) { int nv = (val[u] + val[v]) / 2; adj[u].erase(v); adj[v].erase(u); // small-to-large if (adj[u].size() < adj[v].size()) swap(u, v); vector mv(adj[v].begin(), adj[v].end()); for (int w : mv) { adj[w].erase(v); adj[w].insert(u); adj[u].insert(w); } adj[v].clear(); alive[v] = false; val[u] = nv; return u; } }; int run_phase( DynTree& d, function ok ) { queue> q; for (int u = 0; u < d.n; u++) { if (!d.alive[u]) continue; for (int v : d.adj[u]) { if (u < v) q.push({u, v}); } } int got = 0; while (!q.empty()) { auto [u, v] = q.front(); q.pop(); if (!d.connected(u, v)) continue; if (!ok(d.val[u], d.val[v])) continue; int w = d.merge_edge(u, v); got++; for (int z : d.adj[w]) { q.push({w, z}); } } return got; } // ============================================================ // 1. // 「同じ値の辺は全部先に縮約して損しない」 // → その後は適当に合法辺を縮約 // // 0-2-2-0 などで死亡 // ============================================================ int wrong1(const Input& in) { DynTree d(in); int ans = 0; ans += run_phase( d, [](int x, int y) { return x == y; } ); ans += run_phase( d, [](int x, int y) { return legal(x, y); } ); return ans; } // ============================================================ // 2. // 完全にフェーズ固定 // // 同値縮約 // → 0-2 // → 1-1 // // という順番だけで処理 // ============================================================ int wrong2(const Input& in) { DynTree d(in); int ans = 0; ans += run_phase( d, [](int x, int y) { return x == y; } ); ans += run_phase( d, [](int x, int y) { return is02(x, y); } ); ans += run_phase( d, [](int x, int y) { return x == 1 && y == 1; } ); return ans; } // ============================================================ // 3. // 「木なら葉から貪欲でよい」 // // 縮約可能な葉辺だけをどんどん縮約する。 // 1-0-2-0 などで死亡。 // ============================================================ int wrong3(const Input& in) { DynTree d(in); int ans = 0; queue q; for (int v = 0; v < d.n; v++) { if (d.adj[v].size() == 1) q.push(v); } while (!q.empty()) { int v = q.front(); q.pop(); if (!d.alive[v]) continue; if (d.adj[v].size() != 1) continue; int u = *d.adj[v].begin(); if (!legal(d.val[v], d.val[u])) continue; int w = d.merge_edge(v, u); ans++; if (d.adj[w].size() == 1) { q.push(w); } for (int z : d.adj[w]) { if (d.adj[z].size() == 1) { q.push(z); } } } return ans; } // ============================================================ // 4. // 「0-2 -> 1 が特殊だから、とにかく 0-2 を優先」 // // 0-2 が存在する限り最優先。 // なければ他の合法辺。 // ============================================================ int wrong4(const Input& in) { DynTree d(in); int ans = 0; queue> q02; queue> qo; auto push_edge = [&](int u, int v) { if (!d.connected(u, v)) return; int x = d.val[u]; int y = d.val[v]; if (!legal(x, y)) return; if (is02(x, y)) { q02.push({u, v}); } else { qo.push({u, v}); } }; for (int u = 0; u < d.n; u++) { for (int v : d.adj[u]) { if (u < v) push_edge(u, v); } } while (true) { pair e = {-1, -1}; while (!q02.empty()) { auto [u, v] = q02.front(); q02.pop(); if ( d.connected(u, v) && is02(d.val[u], d.val[v]) ) { e = {u, v}; break; } } if (e.first == -1) { while (!qo.empty()) { auto [u, v] = qo.front(); qo.pop(); if ( d.connected(u, v) && legal(d.val[u], d.val[v]) && !is02(d.val[u], d.val[v]) ) { e = {u, v}; break; } } } if (e.first == -1) break; int w = d.merge_edge(e.first, e.second); ans++; for (int z : d.adj[w]) { push_edge(w, z); } } return ans; } // ============================================================ // 0-2 辺だけからなる maximum matching // ============================================================ int matching02(const Input& in) { int n = in.n; // dp[v][0]: // v は親とはマッチしていない // // dp[v][1]: // v は親とマッチ済み vector> dp(n); function dfs = [&](int v, int p) { int base = 0; for (int u : in.g[v]) { if (u == p) continue; dfs(u, v); base += dp[u][0]; } dp[v][1] = base; dp[v][0] = base; for (int u : in.g[v]) { if (u == p) continue; if (!is02(in.a[v], in.a[u])) continue; dp[v][0] = max( dp[v][0], base - dp[u][0] + dp[u][1] + 1 ); } }; dfs(0, -1); return dp[0][0]; } // ============================================================ // 5. // 「同値辺は取る + // 0-2 辺は maximum matching」 // // 1-0-2 など、生成した1の価値を評価できず死亡。 // ============================================================ int wrong5(const Input& in) { int same = 0; for (int v = 0; v < in.n; v++) { for (int u : in.g[v]) { if (v < u && in.a[v] == in.a[u]) { same++; } } } return min( in.n - 1, same + matching02(in) ); } // ============================================================ // 6. // 「0/2 の連結成分に 0 と 2 が両方存在すれば、 // その成分全体を1にできる」 // // 星型などで死亡。 // ============================================================ int wrong6(const Input& in) { int n = in.n; DSU uf(n); // 1 を除いた 0/2 成分 for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if ( in.a[v] != 1 && in.a[u] != 1 ) { uf.unite(v, u); } } } vector sz(n); vector has0(n); vector has2(n); for (int v = 0; v < n; v++) { if (in.a[v] == 1) continue; int r = uf.root(v); sz[r]++; has0[r] |= (in.a[v] == 0); has2[r] |= (in.a[v] == 2); } int ans = 0; // 0/2成分内部は全部縮約可能だと決めつける for (int r = 0; r < n; r++) { if (sz[r]) { ans += sz[r] - 1; } } // mixed な成分は「1になれる」とみなす for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if (in.a[v] == 1 && in.a[u] == 1) { ans++; } else if ( in.a[v] == 1 && in.a[u] != 1 ) { int r = uf.root(u); if (has0[r] && has2[r]) { ans++; } } else if ( in.a[u] == 1 && in.a[v] != 1 ) { int r = uf.root(v); if (has0[r] && has2[r]) { ans++; } } } } return min(n - 1, ans); } // ============================================================ // 同値連結成分を縮約 // ============================================================ struct CompTree { int m; int internal; vector> g; vector val; vector sz; }; CompTree compress_equal(const Input& in) { int n = in.n; DSU uf(n); for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if ( v < u && in.a[v] == in.a[u] ) { uf.unite(v, u); } } } vector id(n, -1); int m = 0; for (int v = 0; v < n; v++) { if (uf.root(v) == v) { id[v] = m++; } } vector rid(n); for (int v = 0; v < n; v++) { rid[v] = id[uf.root(v)]; } CompTree ct; ct.m = m; ct.internal = n - m; ct.g.assign(m, {}); ct.val.assign(m, 0); ct.sz.assign(m, 0); for (int v = 0; v < n; v++) { int c = rid[v]; ct.val[c] = in.a[v]; ct.sz[c]++; } for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if (rid[v] == rid[u]) continue; ct.g[rid[v]].push_back(rid[u]); ct.g[rid[u]].push_back(rid[v]); } } return ct; } // ============================================================ // 7. // 「同値成分を1頂点に完全縮約してから // 4状態DPすればよい」 // // port情報を失う。 // 0-0 の異なる頂点に 2 が付く場合などを区別できない。 // ============================================================ int wrong7(const Input& in) { auto ct = compress_equal(in); int m = ct.m; vector> dp(m); function dfs = [&](int v, int p) { // 0: 非1 // 1: 将来1 // 2: 1かつ余裕あり // 3: 1かつ余裕なし array cur = { NEG, NEG, NEG, NEG }; if (ct.val[v] == 1) { cur[2] = 0; } else { cur[0] = 0; cur[1] = 0; } for (int u : ct.g[v]) { if (u == p) continue; dfs(u, v); array nxt = { NEG, NEG, NEG, NEG }; for (int k = 0; k < 4; k++) { if (cur[k] <= NEG / 2) continue; for (int j = 0; j < 4; j++) { if (dp[u][j] <= NEG / 2) continue; // 辺を使わない // 子の予約状態はここでは残せない if (j != 1) { nxt[k] = max( nxt[k], cur[k] + dp[u][j] ); } // 両方1なら 1-1 として縮約 if ( k != 0 && j != 0 && k != 1 && j != 1 ) { nxt[k] = max( nxt[k], cur[k] + dp[u][j] + 1 ); } // 0-2 を起爆として使用 if ( is02(ct.val[v], ct.val[u]) && k != 0 && j != 0 && k != 3 && j != 3 ) { nxt[3] = max( nxt[3], cur[k] + dp[u][j] + 1 ); } } } cur = nxt; } dp[v] = cur; }; dfs(0, -1); int best = max({ dp[0][0], dp[0][2], dp[0][3] }); return ct.internal + best; } // ============================================================ // 8. // dp[v][0/1/2] // // 「部分木を完全に処理した後の // vを含む塊の最終値だけ持てばよい」 // // 0-0-2 などで予約ができず死亡。 // ============================================================ int wrong8(const Input& in) { int n = in.n; vector> dp(n); function dfs = [&](int v, int p) { array cur = { NEG, NEG, NEG }; cur[in.a[v]] = 0; for (int u : in.g[v]) { if (u == p) continue; dfs(u, v); array nxt = { NEG, NEG, NEG }; for (int x = 0; x < 3; x++) { if (cur[x] <= NEG / 2) continue; for (int y = 0; y < 3; y++) { if (dp[u][y] <= NEG / 2) continue; // 親子辺を残す nxt[x] = max( nxt[x], cur[x] + dp[u][y] ); // 今ここで縮約 if ((x & 1) == (y & 1)) { int z = (x + y) / 2; nxt[z] = max( nxt[z], cur[x] + dp[u][y] + 1 ); } } } cur = nxt; } dp[v] = cur; }; dfs(0, -1); return max({ dp[0][0], dp[0][1], dp[0][2] }); } // ============================================================ // 9. // さらに状態を潰して // // 0: 元の非1色のまま // 1: 1になれる // // だけでDP。 // 「余裕あり1 / 余裕なし1 / 予約」を区別しない。 // ============================================================ int wrong9(const Input& in) { int n = in.n; vector> dp(n); function dfs = [&](int v, int p) { array cur = { NEG, NEG }; if (in.a[v] == 1) { cur[1] = 0; } else { cur[0] = 0; } for (int u : in.g[v]) { if (u == p) continue; dfs(u, v); array nxt = { NEG, NEG }; for (int s = 0; s < 2; s++) { if (cur[s] <= NEG / 2) continue; for (int t = 0; t < 2; t++) { if (dp[u][t] <= NEG / 2) continue; int x = s ? 1 : in.a[v]; int y = t ? 1 : in.a[u]; // 辺を使わない nxt[s] = max( nxt[s], cur[s] + dp[u][t] ); if ((x & 1) == (y & 1)) { int z = (x + y) / 2; int ns = (z == 1); if ( z == 1 || z == in.a[v] ) { nxt[ns] = max( nxt[ns], cur[s] + dp[u][t] + 1 ); } } } } cur = nxt; } dp[v] = cur; }; dfs(0, -1); return max( dp[0][0], dp[0][1] ); } // ============================================================ // 10. // 「初期値1で木を分割し、 // 各 0/2 成分を独立最適化」 // // mixed成分なら隣接する1全部につながる、と考える。 // ============================================================ int wrong10(const Input& in) { int n = in.n; DSU uf(n); for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if ( in.a[v] != 1 && in.a[u] != 1 ) { uf.unite(v, u); } } } vector has0(n); vector has2(n); for (int v = 0; v < n; v++) { if (in.a[v] == 1) continue; int r = uf.root(v); has0[r] |= (in.a[v] == 0); has2[r] |= (in.a[v] == 2); } int ans = matching02(in); // 同値辺は全部安全だと仮定 for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if ( v < u && in.a[v] == in.a[u] ) { ans++; } } } // mixed component は隣の1全部と接続可能と仮定 for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if ( in.a[v] == 1 && in.a[u] != 1 ) { int r = uf.root(u); if (has0[r] && has2[r]) { ans++; } } if ( in.a[u] == 1 && in.a[v] != 1 ) { int r = uf.root(v); if (has0[r] && has2[r]) { ans++; } } } } return min(n - 1, ans); } // ============================================================ // 11. // 木構造を無視。 // #0, #1, #2 だけで答えを計算。 // ============================================================ int wrong11(const Input& in) { int c[3] = {}; for (int x : in.a) { c[x]++; } int k = min(c[0], c[2]); // 0+2 を k 回 int ans = k; // 作った1と元の1を全部連結できると仮定 int ones = c[1] + k; if (ones) { ans += ones - 1; } // 残った0同士・2同士も全部連結できると仮定 int r0 = c[0] - k; int r2 = c[2] - k; if (r0) ans += r0 - 1; if (r2) ans += r2 - 1; return min(in.n - 1, ans); } // ============================================================ // 12. // 0/2領域について // // 「0の同値成分数」と // 「2の同値成分数」の差 // // だけで残る個数が決まる、と仮定。 // ============================================================ int wrong12(const Input& in) { int n = in.n; DSU mono(n); DSU region(n); for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if (v >= u) continue; if ( in.a[v] != 1 && in.a[u] != 1 ) { region.unite(v, u); } if ( in.a[v] != 1 && in.a[v] == in.a[u] ) { mono.unite(v, u); } } } unordered_map> verts; for (int v = 0; v < n; v++) { if (in.a[v] != 1) { verts[region.root(v)].push_back(v); } } int ans = 0; for (auto& [r, vs] : verts) { set zero_comp; set two_comp; for (int v : vs) { if (in.a[v] == 0) { zero_comp.insert(mono.root(v)); } else { two_comp.insert(mono.root(v)); } } int c0 = zero_comp.size(); int c2 = two_comp.size(); // 差だけ残る、と仮定 int rem = max( 1, abs(c0 - c2) ); ans += (int)vs.size() - rem; } // 1-1 は普通に全部取る for (int v = 0; v < n; v++) { for (int u : in.g[v]) { if ( v < u && in.a[v] == 1 && in.a[u] == 1 ) { ans++; } } } return min(n - 1, ans); } // ============================================================ // 13. // 「0/2 頂点を全部覆う perfect matching があれば // 全部縮約可能」 // // なければ matching 中心で数える。 // ============================================================ int wrong13(const Input& in) { int non1 = 0; int c1 = 0; for (int x : in.a) { non1 += (x != 1); c1 += (x == 1); } int m = matching02(in); if (2 * m == non1) { return in.n - 1; } int same = 0; for (int v = 0; v < in.n; v++) { for (int u : in.g[v]) { if ( v < u && in.a[v] == in.a[u] ) { same++; } } } int ans = same + m; // matching で作った1と元の1を全部合体できると仮定 if (c1 + m > 0) { ans += max( 0, c1 + m - 1 ); } return min(in.n - 1, ans); } // ============================================================ // component tree 上での b-matching // // 「同値成分は capacity 個まで // 0-2 境界辺を使える」 // // と雑に扱う。 // ============================================================ int bmatching_on_components( const CompTree& ct, bool capTwo ) { int m = ct.m; // dp[v][parent edge selected?] vector> dp(m); function dfs = [&](int v, int p) { int base = 0; vector gain; for (int u : ct.g[v]) { if (u == p) continue; dfs(u, v); base += dp[u][0]; if (is02(ct.val[v], ct.val[u])) { gain.push_back( 1 + dp[u][1] - dp[u][0] ); } } sort( gain.rbegin(), gain.rend() ); int cap; if (ct.val[v] == 1) { cap = 0; } else { cap = ct.sz[v]; } if (capTwo) { cap = min(cap, 2); } for (int pe = 0; pe <= 1; pe++) { if (pe > cap) { dp[v][pe] = NEG; continue; } int take = cap - pe; int res = base; for ( int i = 0; i < (int)gain.size() && i < take; i++ ) { if (gain[i] > 0) { res += gain[i]; } } dp[v][pe] = res; } }; dfs(0, -1); return dp[0][0]; } // ============================================================ // 14. // 同値成分を縮約し、 // // 「成分サイズ = 使えるport数」 // // とみなす。 // // 境界辺が同じ元頂点に集中していることを無視。 // ============================================================ int wrong14(const Input& in) { auto ct = compress_equal(in); return min( in.n - 1, ct.internal + bmatching_on_components( ct, false ) ); } // ============================================================ // 15. // 「長い同値列は2頂点程度残せば十分」 // // 各同値成分の port capacity を高々2に潰す。 // 3個以上の独立portが必要なケースで死亡。 // ============================================================ int wrong15(const Input& in) { auto ct = compress_equal(in); return min( in.n - 1, ct.internal + bmatching_on_components( ct, true ) ); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); // ====================================== // ここだけ 1〜15 に変更 // ====================================== constexpr int MODE = 10; Input in = read_input(); int ans = 0; if (MODE == 1) ans = wrong1(in); if (MODE == 2) ans = wrong2(in); if (MODE == 3) ans = wrong3(in); if (MODE == 4) ans = wrong4(in); if (MODE == 5) ans = wrong5(in); if (MODE == 6) ans = wrong6(in); if (MODE == 7) ans = wrong7(in); if (MODE == 8) ans = wrong8(in); if (MODE == 9) ans = wrong9(in); if (MODE == 10) ans = wrong10(in); if (MODE == 11) ans = wrong11(in); if (MODE == 12) ans = wrong12(in); if (MODE == 13) ans = wrong13(in); if (MODE == 14) ans = wrong14(in); if (MODE == 15) ans = wrong15(in); cout << ans << '\n'; }