选课——动态规划
题目来源
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有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
解题报告
题目中提到每门课的直接先修课最多只有一门,将每门课的先修课与其相连,将会构成一颗或者多颗树。为此我们可以建立一个 0 号节点,将所有树的根节点都连向 0 号节点。0 号节点的学分为 0,并且允许选修的课数量要自加一。
将森林拼接成一棵树之后我们就需要对这一颗树进行树上的动态规划了。
考虑树上的每一个节点:这个节点可能是一个或者多个节点的父节点(先修课),也有可能没有。用 f[i][j] 表示对于节点 i , 一共选择 j 门课所能得到的最大学分(j 门科目中包括他本身)。选择他的所有子节点的前提是一定要选择这个节点,所 f[i][1] 一定等于 i 节点的权值(学分)
按照一定的顺序遍历 i 节点的所有子节点。当遍历到 x 节点的时候,我们假设已经得到了所有 f[x][k] 的值。由于 f[i][k] 中是没有 x 节点及其子树的权值的,所以可以通过 x 节点来更新 i 节点的 f 数组。即 f[i][k] = max(f[i][k], f[i][k - p] + f[x][p]) (1 ≤ k ≤ m , 0 ≤ p < k)
p < k 的原因是当 p = k 的时候 f[i][k - p] 中 k - p 的值等于 0,这说明 i 这个节点无法被选择到,由于 i 是 x 节点的先修课,不选择 i 节点便无法选择 x 节点,所以应满足 p < k
在枚举 k 的时候需要注意:由于 f[i][k] 由 f[i][k - p] 更新得到,那么就需要保证更新 f[i][k] 的时候 f[i][k - p] 没有被更新,由于 p 是一个正整数,所以我们倒序枚举 k 即可
源代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, m;
int score[305], fa[305];
bool vis[305];
int f[305][305];
int head[305], edge_len;
struct Edge {
int to;
int next;
} edges[605];
void add(int from, int to) {//加边
edges[++edge_len].to = to;
edges[edge_len].next = head[from];
head[from] = edge_len;
}
void dfs(int x) {
f[x][1] = score[x];
int to;
for (int i = head[x]; ~i; i = edges[i].next) { // 遍历 x 的所有子节点
to = edges[i].to; // to 是 x 的一个子节点
dfs(to); // 计算 to 节点的 f 数组
for (int j = m; j >= 1; j--)//倒序枚举,使用 to 节点的 f 数组更新 x 节点的 f 数组
for (int i = j - 1; i >= 1; i--)
f[x][j] = max(f[x][j], f[x][j - i] + f[to][i]);
}
}
int main() {
freopen("in.txt", "r", stdin);
scanf("%d%d", &n, &m);
memset(head, -1, sizeof(head));
for (int i = 1; i <= n; i++) {
scanf("%d%d", &fa[i], &score[i]);
add(fa[i], i);//由父节点指向子节点的单向边
}
m++; // 由于 0 号节点的参与,m 需要自加1
dfs(0);//搜索 0 号节点
cout << f[0][m];
return 0;
}