HDU7096. Subtraction 樹上貪心
HDU7096. Subtraction
題意:
給定一棵\(n\)個節點的無根樹,節點編號由\(1\)到\(n\),節點\(i\)有正整數權值\(b_i\)和度數限制\(p_i\)。你可以進行如下操作若干次:
選擇這棵樹的一個連通塊,滿足每個在該連通塊內的節點\(i\)在該連通塊中的度數不超過\(p_i\)。對於屬於該連通塊的所有節點\(i\),令\(b_i\)減少\(1\)。
求最少的操作次數使得每個節點的\(b_i\)均變為\(0\)。
分析:
我們嘗試隨意定義一個節點為根節點,不妨令\(1\)為根節點,那麼這棵樹的父子關係就確定了。
考慮對於一個根節點,若以其所有子節點為根節點的子樹的最優操作方法都已經計算出來,我們能不能通過這些子樹的最優操作方法得到整個樹的最優操作方法。
先考慮子樹最後嘗試和父節點合併的情況(即子樹內部得到最優解後,再與父節點合併,從直觀上來說,這種情況最容易考慮)。假設這個父節點是\(i\)節點,要使得它的\(b_i\)變為\(0\),就需要操作\(b_i\)次,這\(b_i\)個操作,每個都是最多和\(p_i\)個子樹合併,我們希望合併的次數越多越好,故而可以遍歷子樹的最優操作方法,檢視這些操作之中哪些可以與父節點合併,優先和父節點中被合併次數少的操作進行合併,這個貪心很顯然是正確的,我們就得到了子樹最後和父節點合併的情況的最優解。
再考慮子樹先和父節點合併產生最優解的情況,對於這個情況,我可以做到撤消某一個與父節點合併的操作,使得某個子樹變得不是最優解,即子樹內部至少可以再進行一次合併操作,所以先和子樹合併並不會使答案變得更劣。
整體思路如此,細節部分每個人想法不同。
我的做法:考慮開一個re
陣列,re[i]
表示\(i\)節點上有多少操作可以和父節點合併。rem
表示當前節點上還能合併多少個操作。合併的時候注意細節。
程式碼:
#include <algorithm> #include <cstdio> #include <vector> using namespace std; typedef long long Lint; const int maxn = 2e5 + 10; vector<int> G[maxn]; Lint ans; Lint b[maxn]; int p[maxn]; Lint re[maxn]; void dfs(int u, int f) { for (auto v : G[u]) { if (v == f) continue; dfs(v, u); } Lint rem = b[u] * p[u]; for (auto v : G[u]) { if (v == f) continue; // 考慮u和v子節點合併 // 不能超過v的可合併數,不能超過u節點的運算元,也不能超過u節點剩餘可合併次數 Lint tmp = min(min(re[v], b[u]), rem); // 多出來的就算可以合併也沒機會再合併了,統計進答案 ans += re[v] - tmp; rem -= tmp; } re[u] = min(b[u], rem); // 那些沒法與父節點合併的操作,統計進答案 ans += b[u] - re[u]; } int n; void solve() { ans = 0; scanf("%d", &n); for (int i = 1; i <= n; i++) { G[i].clear(); } for (int i = 1; i < n; i++) { int u, v; scanf("%d%d", &u, &v); G[u].push_back(v); G[v].push_back(u); } for (int i = 1; i <= n; i++) { scanf("%lld%d", &b[i], &p[i]); } dfs(1, 0); // 根節點多出來的也沒機會合並了,統計進答案 ans += re[1]; printf("%lld\n", ans); } int main() { int T; scanf("%d", &T); while (T--) solve(); return 0; }