// GNU++17 // 時間計算量: O(N log N) // 空間計算量: O(N) // // 重心を両方の子問題に残す二分分解。 // 各分割を BFS 順 + 静的な単調 CHT で線形時間処理する。 #include using namespace std; using i64 = long long; using i128 = __int128_t; struct Edge { int u; int v; }; // 傾き非減少順に全直線を追加した後、 // x 非減少順に最大値クエリを行う。 struct MonotoneMaxCHT { struct Line { i64 m; i64 b; i64 eval(i64 x) const { return m * x + b; } }; vector lines; size_t ptr = 0; void reserve(size_t capacity) { lines.reserve(capacity); } static bool redundant( const Line& a, const Line& b, const Line& c ) { // intersection(a, b) >= intersection(b, c) // 交差積は 64 bit を超え得るので 128 bit で比較する。 return static_cast(a.b - b.b) * (c.m - b.m) >= static_cast(b.b - c.b) * (b.m - a.m); } void add(i64 m, i64 b) { Line line{m, b}; if (!lines.empty() && lines.back().m == m) { if (lines.back().b >= b) { return; } lines.pop_back(); } while (lines.size() >= 2) { size_t n = lines.size(); if (!redundant(lines[n - 2], lines[n - 1], line)) { break; } lines.pop_back(); } lines.push_back(line); } i64 query(i64 x) { while ( ptr + 1 < lines.size() && lines[ptr].eval(x) <= lines[ptr + 1].eval(x) ) { ++ptr; } return lines[ptr].eval(x); } }; vector A; vector F; // 元頂点番号から、その子問題における局所頂点番号への対応。 // 子問題ごとに N 要素を初期化せず、使用した場所だけ戻す。 vector local_id; // この子問題で左右をまたぐパスを処理し、 // 辺集合を二分して返す。共有する重心は両方に残る。 array, 2> process(const vector& edges) { const int m = static_cast(edges.size()); const int n = m + 1; // この子問題の辺だけから局所的な隣接リストを構築する。 // 元の隣接リスト全体を毎回走査しないことが重要。 vector original; original.reserve(n); vector head(n, -1); vector to(2 * m); vector nxt(2 * m); int arc_count = 0; auto get_id = [&](int v) -> int { if (local_id[v] == -1) { local_id[v] = static_cast(original.size()); original.push_back(v); } return local_id[v]; }; auto add_arc = [&](int u, int v) { to[arc_count] = v; nxt[arc_count] = head[u]; head[u] = arc_count; ++arc_count; }; for (const Edge& e : edges) { int u = get_id(e.u); int v = get_id(e.v); add_arc(u, v); add_arc(v, u); } for (int v : original) { local_id[v] = -1; } // 仮に頂点 0 を根として BFS する。 vector parent(n, -1); vector order; order.reserve(n); order.push_back(0); for (size_t i = 0; i < order.size(); ++i) { int v = order[i]; for (int e = head[v]; e != -1; e = nxt[e]) { int u = to[e]; if (u == parent[v]) { continue; } parent[u] = v; order.push_back(u); } } // 通常の頂点重心を求める。 vector subtree_size(n, 1); int c = -1; for (int i = n - 1; i >= 0; --i) { int v = order[i]; int largest = 0; for (int e = head[v]; e != -1; e = nxt[e]) { int u = to[e]; if (u == parent[v]) { continue; } subtree_size[v] += subtree_size[u]; largest = max(largest, subtree_size[u]); } largest = max(largest, n - subtree_size[v]); if (largest <= n / 2) { c = v; } } // 重心 c を根として BFS し直す。 // // branch[v]: v が属する、c を除いた連結成分の番号 // depth[v] : c からの辺数 // B[v] : c-v パスの頂点値の和 - T(depth[v]) vector depth(n, 0); vector branch(n, -1); vector branch_size; branch_size.reserve(n); vector B(n); const i64 ac = A[original[c]]; order.clear(); order.push_back(c); parent[c] = -1; B[c] = ac; for (size_t i = 0; i < order.size(); ++i) { int v = order[i]; for (int e = head[v]; e != -1; e = nxt[e]) { int u = to[e]; if (u == parent[v]) { continue; } parent[u] = v; depth[u] = depth[v] + 1; // T(d) - T(d - 1) = d B[u] = B[v] + A[original[u]] - depth[u]; if (v == c) { branch[u] = static_cast(branch_size.size()); branch_size.push_back(0); } else { branch[u] = branch[v]; } ++branch_size[branch[u]]; order.push_back(u); } } // 枝を二分し、両側の辺数を [m/3, 2m/3] にする。 // 枝の頂点数は、c との接続辺を含めた枝の辺数に等しい。 const int k = static_cast(branch_size.size()); vector branch_side(k, 1); int left_edges = 0; for (int i = 0; i < k; ++i) { if (3 * branch_size[i] >= m) { branch_side[i] = 0; left_edges = branch_size[i]; break; } } if (left_edges == 0) { for (int i = 0; i < k; ++i) { branch_side[i] = 0; left_edges += branch_size[i]; if (3 * left_edges >= m) { break; } } } // c を通る u-v パスの得点: // B[u] + B[v] - A[c] - depth[u] * depth[v] // // 各側について直線 -depth[v] * x + B[v] を登録する。 MonotoneMaxCHT hulls[2]; hulls[0].reserve(left_edges + 1); hulls[1].reserve(m - left_edges + 1); // 逆 BFS 順なら距離は非増加、傾きは非減少。 for (int i = n - 1; i >= 1; --i) { int v = order[i]; int side = branch_side[branch[v]]; hulls[side].add(-static_cast(depth[v]), B[v]); } // c を端点にするパスも許す。 // 傾き 0 は最大なので、最後に追加する。 hulls[0].add(0, ac); hulls[1].add(0, ac); vector best(n, -(1LL << 60)); best[c] = ac; // BFS 順ならクエリ位置 depth[v] は非減少。 // 両側の全頂点について、反対側の hull に問い合わせる。 for (int i = 1; i < n; ++i) { int v = order[i]; int side = branch_side[branch[v]]; best[v] = B[v] - ac + hulls[side ^ 1].query(depth[v]); } // best[v] を部分木最大値にして、元頂点の答えへ反映する。 // // 伝播するのは今回の重心を通るパスの値だけ。 // グローバルな F の値を親へ伝播してはいけない。 for (int i = n - 1; i >= 0; --i) { int v = order[i]; F[original[v]] = max(F[original[v]], best[v]); if (parent[v] != -1) { best[parent[v]] = max(best[parent[v]], best[v]); } } // 子問題を元の頂点番号による辺リストで作る。 // 辺は重複せず、重心 c だけが両方の子問題に現れる。 array, 2> children; children[0].reserve(left_edges); children[1].reserve(m - left_edges); for (int i = 1; i < n; ++i) { int v = order[i]; int side = branch_side[branch[v]]; children[side].push_back({ original[parent[v]], original[v] }); } return children; } void decompose(vector edges) { if (edges.empty()) { return; } if (edges.size() == 1) { int u = edges[0].u; int v = edges[0].v; i64 score = A[u] + A[v] - 1; F[u] = max(F[u], score); F[v] = max(F[v], score); return; } auto children = process(edges); // 再帰前に親の辺リストを解放する。 // process() 内の作業領域も、この時点で解放済み。 vector().swap(edges); decompose(move(children[0])); decompose(move(children[1])); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N; cin >> N; A.resize(N); for (i64& x : A) { cin >> x; } // 単一頂点のパスを最初から考慮する。 F = A; local_id.assign(N, -1); vector edges(N - 1); for (Edge& e : edges) { cin >> e.u >> e.v; --e.u; --e.v; } decompose(move(edges)); cout << *min_element(F.begin(), F.end()) << '\n'; return 0; }