Description
給定一棵 \(n\) 個點的帶權樹,要求選 \(k\) 個點染成黑色,剩下染成白色,最大化兩兩同色點之間的距離和。
Limitations
\(0 \leq k \leq n \leq 2000\)
Solution
首先看一個trick:
考慮如下遍歷一棵樹的僞代碼:
func dfs(u):
size[u] <- 1
for v in clild[u] do
dfs(v)
for i = 1 : size[u] do
for j = 1: size[v] do
do sth
end
end
size[u] <- size[u] + size[v]
end
end func
如果認爲do sth的複雜度是 \(O(T)\) 的話,那麼上述僞代碼的時間複雜度是 \(O(n^2T)\),也就是說上述遍歷整棵樹外加 \(3\) 層 for 循環的時間複雜度是 \(O(n^2)\)。
證明上可以考慮如果記錄每個點的 dfs 序,\(i\) 可以看作枚舉 \(u\) 所有已經到達過的除 \(v\) 及其子樹以外的後代。\(j\) 可以看作枚舉 \(v\) 的後代。那麼考慮任何一對點都只會在他們的 LCA \(u\) 處被枚舉到,即每對點只會枚舉一次,且 \(i\) 的 dfs 序一定小於 \(j\) 的 dfs 序,由於整個 dfs 序序列的順序對個數又 \(O(n^2)\) 個,所以內層兩層循環的總次數是 \(O(n^2)\) 的。於是上述代碼的時間複雜度是 \(O(n^2T)\),其中 do sth 的時間複雜度是 \(O(T)\)
回到這個題,最直接的DP是設 \(f_{u, i}\) 爲以 \(u\) 爲根的子樹,選了 \(i\) 個黑點的最優答案,但是發現無法轉移,因爲全局最優解和局部最優沒什麼關係,於是考慮像 [NOI2019]回家的路 一樣,對貢獻去做DP。
考慮將上述狀態更換爲以 \(u\) 爲根的子樹,選了 \(i\) 個黑點對答案的最大貢獻是多少,在轉移時考慮每個子樹的貢獻即可。
具體的,設 \(f_{u, i, j}\) 爲以 \(u\) 爲根的子樹,考慮前 \(i\) 個孩子,選了 \(j\) 個黑點的最大貢獻。
那麼轉移顯然:
\[f_{u,i,j} = \max_{h = 0}^{j} f_{u, i-1, h} + f_{v, size[v], j-h} + val\]
其中 \(val\) 是這條邊對答案造成的貢獻,具體爲邊兩側黑點個數的乘積加上兩側白點個數的乘積的和再乘上邊權。
例如子樹中選擇了 \(x\) 個黑點,那麼貢獻爲
\[[x \times (k-x) + (size_v - x) \times (n - k - size_v + x)] \times e_w\]
其中 \(e_w\) 爲邊權。
考慮上述轉移中,\(u\) 的狀態 \(i\) 只依賴於狀態 \(i-1\) 和 \(size_v\),因此可以滾動數組。滾動後的狀態轉移方程爲:
\[f_{u,j} = \max_{h=0}^j f_{u, h} + f_{v, j-h} + val\]
再轉移時需要保證 \(f_{u,h}\) 是沒有被 \(v\) 更新過的答案,也就是說比 \(j\) 小的狀態應該在 \(j\) 之後更新,因此倒序枚舉 \(j\) 即可。
在考慮最後一個問題:我們設計的方程是填表轉移,即枚舉了上面僞代碼中 \(i\) 和 \(j\) 的和以及 \(i\),只有這樣才能保證方程中 \(j\) 的轉移是單調的從而滾動數組,因此我們需要把上述僞代碼改成外層枚舉兩數和,內層枚舉除 \(v\) 以外的已遍歷過的子樹的形式。
func dfs(u):
size[u] <- 1
for v in clild[u] do
dfs(v)
size[u] <- size[u] + size[v]
for sum = 1 : size[u] do
for i = max(0, sum - size[v]) : size[v] do
do sth
end
end
end
end func
雖然寫成這樣的時間複雜度十分難以分析,但是枚舉和再枚舉其中一個的複雜度顯然和枚舉兩個求和的複雜度相同,因此上述代碼的時間複雜度也爲 \(O(n^2T)\)
由於單次轉移是 \(O(1)\) 的,因此算法的總時間複雜度 \(O(n^2)\)
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
const int maxn = 2003;
int n, K, dK;
int sz[maxn];
ll frog[maxn][maxn];
struct Edge {
int v, w;
Edge *nxt;
Edge(const int _v, const int _w, Edge *h) : v(_v), w(_w), nxt(h) {}
};
Edge *hd[maxn];
void dfs(const int u, const int fa);
int main() {
freopen("1.in", "r", stdin);
qr(n); qr(K); dK = n - K;
for (int i = 1, u, v, w; i < n; ++i) {
u = v = w = 0; qr(u); qr(v); qr(w);
hd[u] = new Edge(v, w, hd[u]);
hd[v] = new Edge(u, w, hd[v]);
}
dfs(1, 0);
qw(frog[1][K], '\n', true);
return 0;
}
void dfs(const int u, const int fa) {
sz[u] = 1;
memset(frog[u] + 2, -1, 16008);
for (auto e = hd[u]; e; e = e->nxt) if (e->v != fa) {
int v = e->v; dfs(v, u); sz[u] += sz[v];
for (int i = std::min(sz[u], K); ~i; --i) {
for (int j = i, lim = std::max(0, i - sz[v]); j >= lim; --j) if (~frog[u][j]) {
int k = i - j; if (frog[v][k] == -1) continue;
ll val = (1ll * k * (K - k) + 1ll * (sz[v] - k) * (dK - sz[v] + k)) * e->w;
frog[u][i] = std::max(frog[u][i], frog[u][j] + frog[v][k] + val);
}
}
}
}
Summary
1、上面那種神奇的枚舉方式遍歷整棵樹的時間複雜度是 \(O(n^2)\) 的
2、當問題不滿足最優子結構時,可以考慮DP每個子問題對答案的貢獻。
3、很多時候刷表法難以滾動數組,需要轉化成填表法
appreciation
感謝 @DDOSvoid 大爺與我的討論以及對我的啓發