1. 題目
題目鏈接:P2014「[CTSC1997]選課」 。
題目描述
在大學裏每個學生,爲了達到一定的學分,必須從很多課程裏選擇一些課程來學習,在課程裏有些課程必須在某些課程之前學習,如高等數學總是在其它課程之前學習。現在有 NN 門功課,每門課有個學分,每門課有一門或沒有直接先修課(若課程 a 是課程 b 的先修課即只有學完了課程 a,才能學習課程 b)。一個學生要從這些課程裏選擇 MM 門課程學習,問他能獲得的最大學分是多少?
輸入格式
第一行有兩個整數 , 用空格隔開。(,)
接下來的 行,第 行包含兩個整數 和 , 表示第 門課的直接先修課, 表示第 門課的學分。若 表示沒有直接先修課(,)。
輸出格式
只有一行,選 門課程的最大得分。
輸入輸出樣例
輸入 #1
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
輸出 #1
13
2. 題解
分析
樹上 dp + 揹包問題的結合,即樹上揹包。有題意可知,最終這些課程構成一個森林,我們不妨將 0 號結點看作所有樹的根,其學分爲 0,則我們將森林轉爲一棵樹來處理:
首先構建狀態:設 表示以 號結點爲根容許選 門課的子樹的最大學分。
然後構建狀態轉移方程:遍歷結點 的每個孩子 ,計算轉移方程 即對於當前結點 有 的容量、分 的容量給 子樹得到的最大學分。
注意
- 首先,對於每個孩子 而言, 應當從最大遞減逆序枚舉,因爲需要保證 的最優解不是來源於 子樹(因爲 已經是來源於 子樹的)。
- 其次,遍歷每個孩子 時 的範圍時不一樣的。因爲對於已經遍歷過的子樹而言,我們需要考慮其也有可能對最優解產生貢獻,因此需要將 的範圍擴大到包括所有已遍歷的子樹大小(但不能超過 )。設 爲已經遍歷的子樹的大小之和,則 的範圍爲
- 最後,需要計算結點 所有可能容量的最優解,然後向上更新父結點。以此類推。
求解過程可以採用自頂向下的方法,即利用樹的遞歸性質,採用 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;
}