“最菜”动规
P2014 [CTSC1997]选课
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习。在课程里有些课程必须在某些课程之前学习,如高等数学总是在其他课程之前学习,现在有 N 门功课,每门课有学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课,即只有学完了课程 a ,才能学习课程 b )。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数 N , M 用空格隔开(1 ≤ N ≤ 300,1 ≤ M ≤ 300)。
接下来的 N 行,每行包含两个整数 ki 和 si , ki 表示第 i 门课的直接先修课, si表示第 i 门课的学分。
若 ki 为零表示没有直接先修课(1 ≤ ki ≤ N,1 ≤ si ≤ 20)。
输出格式
只有一行,选 M 门课程的最大可得学分。
输入输出样例
输入 #
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出 #
13
分析
==============================================================================
部分课程形成树,全部课程形成森林
为方便处理,设置虚拟根结点,将各个树的根连接至该虚拟结点
==============================================================================
单独拿出⑦,⑤和⑥,作为整体进行分析(过程一)
以下情况最高可得学分
选择一门课程:⑦ - (2)
选择两门课程:⑦⑥ - (2+6)
选择三门课程:⑦⑥⑤ - (2+6+1)
选择四门课程:⑦⑥⑤ - (2+6+1)
==============================================================================
单独拿出②,①,④,⑦,⑤和⑥,作为整体进行分析(过程二)
以下情况可得最高学分
选择一门课程:② - (1)
选择两门课程:②① / ②[⑦] - (1+2)
选择三门课程:②[⑦⑥] - (1+2+6)
选择四门课程:②①[⑦⑥] - (1+2+2+6)
注意:
其中⑦和⑦⑥可分别看作整体
而⑦看作过程一中的选择一门课程的情况
而⑦⑥看作过程一中的选择两门课程的情况
==============================================================================
最后回到整棵树(过程三)
以下情况可得最高学分
选择一门课程:虚拟结点 - (0)
选择两门课程:③ - (4)
选择三门课程:②③ - (1+4)
选择四门课程:[②①]③ / [②⑦]③ - (1+2+4)
选择五门课程:[②⑦⑥]③ - (1+2+6+4)
得到答案13
注意:
为什么要考虑选择五门课程的情况?
因为虚拟结点必须选上,所以加上题目所要求的课程数量,1+4=5。
补充:
类似地,②①和②⑦和②⑦⑥仍分别看作整体。
==============================================================================
关于状态转移方程:
例如,过程二的选择四门课程的情况
将②,②①,②①④分别相等于②[1],②[2],②[3]
(其实相当于目前在分析父结点②下的孩子结点⑦,所以不包括⑦)
(②[i]都表示选择i门课程下的最高学分)
将⑦,⑦⑥,⑦⑥⑤分别相等于⑦[1],⑦[2],⑦[3]
则,有如下式子
②[4] = max { ②[3] + ⑦[1] , ②[2] + ⑦[2] , ②[1] + ⑦[3] }
太多废话,你看得累我也说得累……
代码
#include <iostream>
using namespace std;
#define MAX 301
int N, M, score[MAX + 1];
int Node[MAX + 1][MAX + 1], pos[MAX + 1];
// Node[i]为第i个结点,Node[i][j]为第i个根的第j个相连结点
// pos[i]用于表示第i个结点的相连结点个数,如果 pos[i] == 1 则Node[i]是叶子结点
int dp[MAX + 1][MAX + 1];
void dfs(int now, int pre) {
for(int i = 0; i < pos[now]; i++) { // 遍历Node[now]即第now个根的孩子
if(dp[now][M + 1] < score[now]) { // 为后续状态转移方程做准备
for(int x = 1; x <= M + 1; x++) {
dp[now][x] = score[now];
}
}
int child = Node[now][i];
if(child == pre) // 遇到叶子直接返回
continue;
dfs(child, now); // 深度搜索至叶子
for(int j = M + 1; j >= 1; j--) {
for(int k = 1; k < j; k++) {
dp[now][j] = max(dp[now][j], dp[now][k] + dp[child][j - k]);
}
}
}
}
int main() {
cin >> N >> M;
for(int i = 1; i <= N; i++) {
int j;
cin >> j >> score[i];
// 构建树
Node[j][pos[j]++] = i;
Node[i][pos[i]++] = j;
}
dfs(0, -1);
cout << dp[0][M + 1];
}
Bribe the Prisoners(2009 Round 1C C)
题目描述
如下图所示,一个监狱里有P个并排着的牢房。从左至右依次编号为1,2,……,P。最初所有的牢房里都住着一个囚犯。相邻的两个牢房之间有一个窗户,,可以通过它与相邻牢房里的囚犯对话。
现在要释放一些囚犯。如果释放某个牢房里的囚犯,其相邻的牢房里的囚犯就会知道,因而发怒暴动。所以,释放某个牢房里的囚犯同时,必须要贿赂两旁相邻牢房里的囚犯一枚金币。另外,为了防止释放的消息在相邻牢房间传开,不仅两旁直接相邻的牢房,所有可能听到消息的囚犯,即直到空牢房为止或直到监狱两端为止,此间的所有囚犯都必须给一枚金币。
现在要释放A1,A2,……,AQ号牢房里的Q名囚犯,释放的顺序还没确定。如果选择所需金币数量精良少的顺序释放,最少需要多少枚金币?
限制条件
·1 ≤ N ≤ 100
·Q ≤ P
Small
·1 ≤ P ≤ 100
·1 ≤ Q ≤ 5
Large
1 ≤ P ≤ 10000
1 ≤ Q ≤ 100
样例1
输入:P = 8,Q = 1,A = { 3 }
输出:7(必须要给剩下的7个人一枚金币)
样例2
输入:P = 20,Q = 3,A = { 3,6,14 }
输出:35(按照顺序14,6,3的顺序释放,则需要19 + 12 + 4即35枚金币,是最少的)
分析
==============================================================================
借用样例2分析
为方便处理,设置0号空牢房及21号空牢房,即A = { 0,3,6,14,21 },从下标零开始
==============================================================================
单独拿出[0,14]区间进行分析
第一次释放所需金币都为12
第二次释放所需金币分别为
先释放A2 = 6,再释放A1 = 3的情况:4
先释放A1 = 3,再释放A2 = 6的情况:9
由此可得先释放A2 = 6,再释放A1 = 3,所花金币为16
==============================================================================
对[0,21]区间进行分析
第一次释放所需金币都为19
第二次释放所需金币分别为
先释放A3 = 14,再释放(A1 = 3,A2 = 6)的情况:16
(A1 = 3,A2 = 6)看作整体
先释放(A1 = 3,A2 = 6),再释放A3 = 14的情况:4 + 13
4的由来:在释放A2 = 6后释放A1 = 3所得的
由此可得由A3 = 14,A2 = 6,A1 = 3的顺序释放,可使花费金币最少,为35
注意 / 补充
正常比较应该分两组:
(A1 = 3,A2 = 6)与A3 = 14
A1 = 3与(A2 = 6,A3 = 14)
==============================================================================
关于状态转移方程:
Result[A0,A4]((0,21)区间内)
= min { Result[A0,A3] + Result[A3,A4] , Result[A0,A1] + Result[A1,A4] } + A4 - A0 - 2
太多废话,你看得累我也说得累
代码
#include <iostream>
using namespace std;
#define MAX_Q 101
int P, Q, A[MAX_Q + 2];
// A[MAX_Q + 2]中保存输入数据
// 下标从一开始
// 下标[0]为左空牢房或监狱左端
// 下标[Q + 1]为右空牢房或监狱右端
int dp[MAX_Q + 1][MAX_Q + 2]; // 释放(Ai,Aj)所需的至少的金币
void solve() {
A[0] = 0; // 左空牢房或监狱左端
A[Q + 1] = P + 1; // 右空牢房或监狱右端
for(int q = 0; q <= Q; q++) {
dp[q][q + 1] = 0; // 区间内无即将释放的囚犯
}
for(int w = 2; w <= Q + 1; w++) { // 从短的区间开始
for(int i = 0; i + w <= Q + 1; i++) {
int j = i + w, t = INT_MAX;
for(int k = i + 1; k < j; k++) { // 计算第二人释放时最少的金币
t = min(t, dp[i][k] + dp[k][j]);
}
dp[i][j] = A[j] - A[i] - 2 + t; // A[j] - A[i] - 2 为释放第一人时的金币
}
}
cout << dp[0][Q + 1];
}
int main() {
cin >> P >> Q;
for(int i = 1; i <= Q; i++) {
cin >> A[i];
}
solve();
}