P3354 Riv 河流 題解

最近在練樹形 DP,正好看到 這一道 虛標的紫題,但本蒟蒻不會寫,想出來了便記錄一下

題面 & 思考

先看題面,一顆有根樹,選定 k 個節點作為 ”伐木場“,求運送木料最小費用。注意木料費用是 dis * wood

最開始想到簡單的樹形揹包,狀態轉移方程:

1
f[i][k] = min(f[j][s] + cost, f[i][k]);

但是注意到,如果某個後代節點如果是伐木場,cost 不需要計算,所以狀態中還需要儲存後代的伐木場情況。

但是後代中鋸木廠不止一個。考慮到樹有唯一的父親,且同一深度祖先唯一,可以記錄最近的伐木場祖先,作為狀態的一部分。

f[i][j][k] 即 i 節點 最近的伐木場祖先為 j,後代(不算自己)有 k 個是伐木場。

但是由於 f 自己也可能是伐木場,轉移方程不同,需要分類討論,於是改成:f[i][j][k][0/1] 其中 0 代表自己不是伐木場, 1 表示自己伐木場。

狀態轉移方程

因為我們需要列舉祖先,在 DFS 時需要記錄 Fa 陣列:

1
2
3
DFS 開始時:fa[++tot] = x;

結束時:tot--;

簡單記錄祖先 stack。

回溯後,對於當前節點 x 和 子節點 y 每個祖先 ff

1
2
3
4
先對每個 k 賦初值,對於每種伐木場個數 l:
f[x][ff][l][0] += f[y][ff][0][0]; --> 當前節點不是伐木場,子節點 y 最近伐木場為 l,對任意 f,賦最大值,即 子結點中沒有伐木場。

f[x][ff][l][1] += f[y][x][0][0]; --> 當前節點是伐木場,子節點 y 最近伐木場為 當前節點,對任意 f,賦最大值,即 子結點中沒有伐木場。

然後就是樹形揹包:

1
2
3
4
f[x][ff][l][0] = min(f[x][ff][l][0], f[x][ff][l-s][0] + f[y][ff][s][0]); --> 當前節點不是伐木場,對 y 節點分配 s 個伐木場個數
f[x][ff][l][1] = min(f[x][ff][l][0], f[x][ff][l-s][1] + f[y][x][s][0]); --> 當前節點是伐木場,對 y 節點分配 s 個伐木場個數

唯一的不同是 y 節點最近祖先為 x

注意在列舉 l 時,由於是01揹包,需要倒過來列舉,不然狀態計算會疊加。(很容易理解)

做完了?好像還差一個 cost!

考慮到 祖先節點 ff cost 的貢獻,因為是 當前節點 W 乘以 到 ff 的總距離!

需要計算總距離,維護 dep 陣列,即當前節點到根節點距離,即可!很容易理解。ffx 的距離就是 dep[x] - dep[ff]

對於當前節點 x 每一個祖先 ff

1
2
3
4
5
6
7
8
if(l>=1){ --> 因為 l 需要 -1 ,要分類討論。
f[x][ff][l][0] = min(f[x][ff][l][0] + W[x] * (dep[x]-dep[ff]), f[x][ff][l][1]);
合併 01,因為回溯之後,1 的狀態不再被使用,便於下一步計算。
-1 是因為上文 f[x][ff][l][1] = min(f[x][ff][l][0], f[x][ff][l-s][1] + f[y][x][s][0]); 時,s+l-s = l 但是當前節點也是伐木場,所以狀態更新是 l+1 的,要 -1 獲取正確結果
}else{
l = 0 時,當前節點不可能為伐木場,直接新增到 ff 增加的貢獻
f[x][ff][l][0] += W[x] * (dep[x]-dep[ff]);
}

這裡再來聊一聊合併。為了便於討論,上文揹包轉移的時候,我們沒有考慮 y 的 1 的情況,而是在每次 y 回溯時 合併 0 和 1。這有點類似於滾動陣列?回溯後 0 不再表示之前的意義,而是我們最初設計的狀態:i 節點 最近的伐木場祖先為 j,後代(或自己)有 k 個是伐木場!

程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <cmath>
#include <cstdio>
#include <iostream>
#include <cstring>

using namespace std;

struct Node {
int to, next, d;
} NDS[201];

int head[201], vis[201];
int cnt = 0;
long long W[201];
int tot = 0;

void add(int a, int b, int d) {
NDS[cnt].to = b;
NDS[cnt].next = head[a];
NDS[cnt].d = d;
head[a] = cnt++;
}

int fa[201];
long long f[201][201][52][2];
long long dep[201];
long long n, k;

void dp(int x) {
fa[++tot] = x;
vis[x] = 1;
for (int i = head[x]; i != -1; i = NDS[i].next) {
int y = NDS[i].to;
if (vis[y]) continue;
dep[y] = dep[x] + NDS[i].d;
dp(y);
for (int j = tot; j >= 1; j--) {
int ff = fa[j];
for (int l = k; l >= 0; l--) {
f[x][ff][l][0] += f[y][ff][0][0];
f[x][ff][l][1] += f[y][x][0][0];
for(int s = l; s>=0; s--){
f[x][ff][l][0] = min(f[x][ff][l][0], f[x][ff][l-s][0] + f[y][ff][s][0]);
f[x][ff][l][1] = min(f[x][ff][l][1], f[x][ff][l-s][1] + f[y][x][s][0]);
}
}
}
}
for (int j = 1; j <= tot; j++) {
int ff = fa[j];
for (int l = k; l >= 0; l--) {
if(l>=1){
f[x][ff][l][0] = min(f[x][ff][l][0] + W[x] * (dep[x]-dep[ff]), f[x][ff][l-1][1]);
}else{
f[x][ff][l][0] += W[x] * (dep[x]-dep[ff]);
}
// cout << x << " "<<ff << " "<< l << " " << f[x][ff][l][0] << " " << endl;
}
}
// cout << dep[x] << endl;
tot--;
}

int main() {
// Type your code here
memset(head, -1, sizeof(head));
cin >> n >> k;
for (int i = 1; i <= n; i++) {
int w, v, d;
cin >> w >> v >> d;
W[i] = w;
add(i, v, d);
add(v, i, d); // 鏈式前向星
}
dp(0);
cout << f[0][0][k][0] << endl; // 輸出結果,注意是 合併後,所以是 0
return 0;
}