選課(LGOJ P2014)—— 樹形DP基礎

目錄

前言

題目

題目描述

輸入輸出格式

輸入輸出樣例

解析

樹形DP

建樹

優化

轉移

參考代碼


前言

已經好久時間沒有更博了。。。

其實這段時間我可沒有玩,而是在很努力地繼續學習來着。。。

最近突然很沉迷於樹形DP來着,於是乎呢,解決出了這道“選課”就來跟大家探討一下。

這道題其實是樹形dp的一道經典題目,而且其中並沒有許多很難思考的關係,因此可以說是初學樹形dp的人們(比方說我這種蒟蒻。。。)必做的一道題啦。

學會這道題以後,我相信其他類似這種的較爲基礎的題也不會難倒大家了。

題目

題目描述

在大學裏每個學生,爲了達到一定的學分,必須從很多課程裏選擇一些課程來學習,在課程裏有些課程必須在某些課程之前學習,如高等數學總是在其它課程之前學習。現在有N門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程a是課程b的先修課即只有學完了課程a,才能學習課程b)。一個學生要從這些課程裏選擇M門課程學習,問他能獲得的最大學分是多少?

輸入輸出格式

輸入格式:

第一行有兩個整數N,M用空格隔開。(1<=N<=300,1<=M<=300)

接下來的N行,第I+1行包含兩個整數ki和si, ki表示第I門課的直接先修課,si表示第I門課的學分。若ki=0表示沒有直接先修課(1<=ki<=N, 1<=si<=20)。

輸出格式:

只有一行,選M門課程的最大得分。

輸入輸出樣例

輸入樣例#1:

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

輸出樣例#1:

13

解析

看到這道題,不知道大家有沒有像我這種的習慣性腦殼疼,不管看到什麼題都十分煩躁  。。。

不過我們再來好好看下,這道題似乎和  金明的預算方案  有一點像,附件就相當於這裏的先修課。But,大家如果再好好想想呢?或者說結合一下樣例來分析分析。

這裏的先修課也許還會有先修課並且一些課程的先修課相同。

那麼想到這裏,我們就能夠知道一件淺顯易懂的事情了:簡單的線性DP已經不能滿足這道題了。

因此,我們就將引出—— 樹形DP

樹形DP

其實,樹形DP並不像我們想象中的那麼難,他就相當於一個在樹上進行的DP罷了,意思也就是說,只是將背景從線性換成了樹而已。因此,我們大可不必害怕,只需要像以前那樣解DP就行了。

那麼該怎麼實現樹形dp呢?其實最簡單的方法,就是通過DFS來實現基礎樹形DP

接下來,我們就來思考怎麼建出這個樹。

建樹

就依照樣例來說吧,或許大家第一時間想到的,就是將先修課作爲它的兒子連起來:

看了這個圖,是不是感覺有點怪怪的樣子??

我們暫且先不管3這個節點吧,光看這個箭頭是不是就不太對?一個節點怎麼能有兩個父親呢??這完全有悖常理吧。。。

因此,我們可以把箭頭調轉一個方向——

嗯嗯嗯,這樣看起來就順眼多了嘛。

不過,爲什麼要這樣建樹呢??其實是可以說出理由的。

大家可以看到,一個有先修課的課程,必須要經過它的先修課後,才能夠學習

這個也可以看做是一棵樹,必須在通過這個節點的父節點後才能到達子節點。

而且在這種情況下,就能夠避免了類似上面的圖的一個錯誤——一個節點或許有多個父親。

優化

可以看到上面一個圖,其實是組成了一個森林,那麼dfs的時候也就會再多一重循環來枚舉每棵樹的根節點,這也正是小題大做了。

因此,我們可以再假想出一個根節點將這些樹都連接起來,把森林變成一棵樹

而這個根節點的不二人選,自然也就是 0號節點 啦。

而且我們也可以從樣例中看出來 (不用再往前翻了,我這裏貼出來)——

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

先修課的先修課自然就是0號節點,那麼我們可以直接通過樣例建樹了。

也就不用再花時間自己再來重新建一遍(我就最討厭這樣了)。

最後的形狀如圖:

轉移

既然已經思考完了建樹,那麼我們就該考慮考慮重頭戲了——dp的轉移

按照平時的慣例,第一維肯定是狀態變量,但是因爲還有選的課程數的限制,因此,我們還得多加一維來存儲已經選了多少門課程

所以就有 {\color{Red} dp_{i, j}} 表示爲以i爲根的子樹選了j門課程的學分的最大值

那麼這個dp到底該如何轉移呢?

我們可以再設一個變量k,表示爲i節點的一個兒子選了k門課程,那麼它的剩餘的兒子們就只能選j - k - 1門課程了

可是爲什麼要減1呢?其實很簡單,因爲還要加上i這門課程啊,如果他不選的話,那麼他的兒子們就一個也選不到了。

至此,我們就推出了轉移公式:

{\color{Red} dp_{i, j} = max\left \{ dp_{sonv, k} + dp_{i, j - k _ 1} + w[i] \right \}}

w[i]就是第i門課程的學分。

參考代碼

#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#define min(a, b) a < b ? a : b
#define max(a, b) a > b ? a : b
 
void read (int &x){
    int f = 1; x = 0;
    char c = getchar ();
    while (c < '0' || c > '9'){
        if (c == '-') f = -1;
        c = getchar ();
    }
    while (c >= '0' && c <= '9'){
        x = (x << 1) + (x << 3) + c - 48;
        c = getchar ();
    }
    x *= f;
}
 
void print (int x){
    if (x < 0){
        putchar ('-');
        x = ~(x) + 1;
    }
    if (x / 10) print (x / 10);
    putchar (x % 10 + 48);
}
 
#define N 300
 
struct edge{
    int v, w;
    edge (){};
    edge (int V, int W){
        v = V; w = W;
    }
};
int n, m, dp[N + 5][N + 5], sz[N + 5];
vector <edge> G[N + 5];
 
void dfs (int u, int f){
    for (int i = 0; i < G[u].size (); i++){
        int v = G[u][i].v;
        if (v == f) continue;
        dfs (v, u);
        sz[u] += sz[v] + 1;
        for (int j = min (sz[u], m); j; j--){
            for (int k = 0; k < j && k <= sz[v]; k++){
                dp[u][j] = max (dp[u][j], dp[v][k] + dp[u][j - k - 1] + G[u][i].w);
            }
        }
    }
}
 
int main (){
    read (n); read (m);
    for (int i = 1; i <= n; i++){
        int u, w;
        read (u); read (w);
        G[u].push_back (edge (i, w));
    }
    dfs (0, -1);
    print (dp[0][m]);
}
 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章