1. 程式人生 > 實用技巧 >DP學習筆記——樹形DP

DP學習筆記——樹形DP

一、樹上揹包

在有依賴關係的揹包問題中,依賴關係通常可以構成森林。建立一個超級源點\(0\)並設為整體的根節點,則可以將森林組織成一棵樹。對於樹中任意一個節點\(v\),要將它放入揹包,必須先將其父節點\(u\)放入揹包。

\(dp[i][j]:=\) 以 根節點為節點\(i\)的的子樹 為物品選擇範圍、在揹包容量為\(j\)時的最大價值。

常見解法是,通過DFS遞迴到葉子節點(假設是第\(n\)層節點),進行01揹包後得出第\(n-1\)層節點的結果,再對第\(n-1\)層節點進行01揹包,得出第\(n-2\)層節點的結果……最後答案彙總至超級源點。

注意,超級源點是必選且是額外選的,對於題目給定的揹包容量\(m\)

,應該在DFS之前擴大它,以恰好裝下超級源點。具體如何擴大,應根據題目而定。例如,在選課這一題中,\(m\)是課程數量,而超級源點算是一門課程,所以在DFS前要進行m++。最後答案為\(dp[0][m]\)\(m\)已被擴大過)。

演算法執行的具體順序如圖所示,藍色為未處理節點,綠色為已計算出子樹最優解的節點,橙色為DFS過程中經過的、正在處理的節點。

葉子節點在輸入時就已完成\(dp\)計算(直接用輸入值作為葉子節點的\(dp\)結果)。

int dp[maxn][maxn];
int num = 0;
int head[maxn];
int n, m;

struct Edge {
    int next, to;
}edges[maxn];

void add_edge(int from, int to) {
    num++;
    edges[num].next = head[from];
    edges[num].to = to;
    head[from] = num;
}
//鏈式前向星存樹

void dfs(int u) {
    for (int i = head[u]; i; i = edges[i].next) 
        dfs(edges[i].to);
    //樹的後序遍歷(dfs序)
    for (int i = head[u]; i; i = edges[i].next) {
        int v = edges[i].to;
        //列舉結點u的所有兒子結點,進行O1揹包
        for (int j = m; j > 0; j--) {
            //01揹包的空間壓縮:容量倒序列舉
            for (int k = 0; k < j; k++) {
                dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[v][k]);
                //dp[u][j]←在dp[u][j-k]的基礎上,再選擇根節點為v的子樹在揹包容量為k時的最優解
            }
        }
    }
}

int main()
{
    //ios::sync_with_stdio(false);
    //while (scanf("%d%d",&n,&m)!=EOF){
    //dp[u][j]表示選擇範圍是以u為根節點的整棵子樹、揹包容量為j時的最大價值
    cin >> n >> m;
    //設一個超級源點,就能將森林組織成一棵樹
    //而本題輸入中已經自帶了一個無先修課的節點0,可以作為超級源點
  	m++;
    //因為必須多選一個超級源點,給它添一個位置
    for (int v = 1; v <= n; v++) {
        int u;
        cin >> u >> dp[v][1];
        //顯然當只有一門課可以選時,選這門課就是揹包容量為1時的最優解
        add_edge(u, v);
    }
    dfs(0);
    cout << dp[0][m];
    return 0;
}