P2014「[CTSC1997]選課」 1. 題目 2. 題解

1. 題目

題目鏈接:P2014「[CTSC1997]選課」

題目描述

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

輸入格式

第一行有兩個整數 NM 用空格隔開。(1 \leq N \leq 3001 \leq M \leq 300

接下來的 N 行,第 I+1 行包含兩個整數 k_is_ik_i 表示第 I 門課的直接先修課,s_i 表示第 I 門課的學分。若 k_i=0 表示沒有直接先修課(1 \leq {k_i} \leq N1 \leq {s_i} \leq 20)。

輸出格式

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

輸入輸出樣例

輸入 #1
7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2
輸出 #1
13

2. 題解

分析

樹上 dp + 揹包問題的結合,即樹上揹包。有題意可知,最終這些課程構成一個森林,我們不妨將 0 號結點看作所有樹的根,其學分爲 0,則我們將森林轉爲一棵樹來處理:

  • 首先構建狀態:設 f[i][j] 表示以 i 號結點爲根容許選 j 門課的子樹的最大學分。

  • 然後構建狀態轉移方程:遍歷結點 u 的每個孩子 v,計算轉移方程 f[i][j] = \max_{1 \le k \le j-1}\{f[i][j], f[i][j-k]+f[v][k]\} 即對於當前結點 ij 的容量、分 k 的容量給 v 子樹得到的最大學分。

注意
  • 首先,對於每個孩子 v 而言,j 應當從最大遞減逆序枚舉,因爲需要保證 f[i][j-k] 的最優解不是來源於 v 子樹(因爲 f[v][k] 已經是來源於 v 子樹的)。
  • 其次,遍歷每個孩子 v_lj 的範圍時不一樣的。因爲對於已經遍歷過的子樹而言,我們需要考慮其也有可能對最優解產生貢獻,因此需要將 j 的範圍擴大到包括所有已遍歷的子樹大小(但不能超過 m+1)。設 res 爲已經遍歷的子樹的大小之和,則 j 的範圍爲 j \in [1,min(res,m+1)]
  • 最後,需要計算結點 i 所有可能容量的最優解,然後向上更新父結點。以此類推。

求解過程可以採用自頂向下的方法,即利用樹的遞歸性質,採用 DFS 遍歷整棵樹,每次遍歷完子樹後再更新當前結點;也可以採用自底向上的方法,首先構建一個隊列,將孩子數爲 0 的結點加入隊列中。然後從隊列中取出結點開始更新,每次更新完都將父結點的孩子樹減 1,然後將孩子樹爲 0 的父結點加入隊列中(本質是一個拓撲序列,將父子結點的邊看作是孩子到父親的有向邊)。

代碼

DFS 計算子樹最優解
#include <bits/stdc++.h>
#define ll int
#define MAXN 305
using namespace std;

// 前向星存邊
ll cnt;
ll head[MAXN];
struct edge{
    ll to;
    ll next;
}e[MAXN];
void init() {
    cnt = 0;
    memset(head, -1, sizeof(head));
}
void addEdge(ll u, ll v) {
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt++;
}

ll k[MAXN];         // 父結點
ll s[MAXN];         // 權值
ll dp[MAXN][MAXN];  

// dfs
ll dfs(ll u, ll m) {
    ll res = 1;
    dp[u][1] = s[u];
    for(ll i = head[u]; i != -1; i = e[i].next) {
        ll curres = dfs(e[i].to, m-1);
        // 剩餘 j 門課
        for(ll j = min(res, m); j; --j) {
            for(ll ii = 1; ii <= curres && j+ii <= m; ++ii) {
                dp[u][j+ii] = max(dp[u][j+ii], dp[u][j] + dp[e[i].to][ii]);
            }
        }
        res += curres;
    }
    return res;
}

int main()
{
    ll n, m;
    init();
    scanf("%d%d", &n, &m);
    for(ll i = 1; i <= n; ++i) {
        ll ki, si;
        scanf("%d%d", &ki, &si);
        k[i] = ki;
        s[i] = si;
        addEdge(ki, i);
    }
    dfs(0, m+1);
    printf("%d\n", dp[0][m+1]);
    return 0;
}

拓撲排序計算子樹最優解

#include <bits/stdc++.h>
#define ll int
#define MAXN 305
using namespace std;

// 前向星存邊
ll cnt;
ll head[MAXN];
struct edge{
    ll to;
    ll next;
}e[MAXN];
void init() {
    cnt = 0;
    memset(head, -1, sizeof(head));
}
void addEdge(ll u, ll v) {
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt++;
}

ll k[MAXN];         // 父結點
ll s[MAXN];         // 權值
ll in[MAXN];        // 入度
ll sz[MAXN];        // 子樹大小
ll dp[MAXN][MAXN];  

void answer(ll n, ll m) {
    queue <ll> q;
    for(int i = 0; i <= n; ++i) {
        if(!in[i]) {
            dp[i][1] = s[i];
            sz[i] = 1;
            --in[k[i]];
            ++sz[k[i]];
            if(!in[k[i]]) {
                ++sz[k[i]];
                q.push(k[i]);
            }
        }
    }
    while(q.size()) {
        ll p = q.front();
        q.pop();
        ll res = 1;
        dp[p][1] = s[p];
        for(ll i = head[p]; i != -1; i = e[i].next) {
            ll u = e[i].to;
            for(ll j = min(res, m+1); j; --j) {
                for(ll d = 1; d <= sz[u] && d+j <= m+1; ++d) {
                    dp[p][d+j] = max(dp[p][d+j], dp[p][j]+dp[u][d]);
                }
            } 
            res += sz[u];
        }
        --in[k[p]];
        sz[k[p]] += sz[p];
        if(!in[k[p]]) {
            ++sz[k[p]];
            q.push(k[p]);
        }
    }
}

int main()
{
    ll n, m;
    init();
    scanf("%d%d", &n, &m);
    for(ll i = 1; i <= n; ++i) {
        ll ki, si;
        scanf("%d%d", &ki, &si);
        k[i] = ki;
        s[i] = si;
        ++in[ki];
        addEdge(ki, i);
    }
    answer(n, m);
    printf("%d\n", dp[0][m+1]);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章