• 【洛谷P2014】选课


    题目描述

    在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有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

    算法:

    树形DP

     

    分析:

    这道题一拿起来就感觉它好像是一个背包问题。但其实普通的有依赖性背包问题是无法解决这种有互相依赖的像图一样的问题,然而我们又发现先修课和后续课存在着关联,于是我们就可以用树形DP。

     

    假如按照之前套路走,我们应该构建一棵多叉树,在这棵树上进行动规。嗯,没问题。

     

    但是,第一数据范围可能会卡掉几个点,第二,代码量会非常大,而且状态转移很难写。

     

    那么我们用图论的方法吧!

     

    嗯,没问题。

     

    但是请想想图论的算法解决这道题又有点杀鸡用牛刀了。而且码量绝对不小。

     

    那么,我们可以用多叉树转二叉树的思想来解决这个问题。

     

    怎么转呢?

     

    先想一个问题,在一棵多叉树中,兄弟的兄弟还是兄弟,儿子的兄弟也还是自己的儿子。兄弟的儿子就与自己没什么关联。

     

    那么我们通过这个性质,构造一棵二叉树,令其满足任意一棵子树的根节点的非空左儿子是自己原来的儿子,非空右儿子是自己原来的兄弟。那么一棵树就建好了。

     

    然后,想状态转移方程。自己的兄弟是和自己有同等的发展机遇的,所以根节点能经历的事,能分到的课程数,它现在的右儿子(兄弟)也同样能经历。所以也要先遍历。

    至于儿子,可以儿子和兄弟一起分课程,那么一起去两个儿子也是可行的。

    那么我们得到以下方程:f[root][len]=max(f[root][len],max(f[b[root]][len],f[b[root]][i]+f[c[root]][len-i-1]+w[root]));

    设f[root][len]表示以root为根节点的时候取len门课程可以取得的最大收益。那么状态转移的状态就有完全不变、不选根节点(只选兄弟)和孩子兄弟一起分这三种情况,很好理解。

     

    对于存储,可以选择链式前向星,用存图的方式来存储,那么dfs的时候就有些奇怪。

    也可以用类似链表的形式存上一条边,同样快捷。

     

    上代码:

     1 #include<cstdio>
     2 #include<iostream>
     3 #include<cstring>
     4 #include<cctype>
     5 using namespace std;
     6 
     7 int w[310],b[310],c[310],f[310][310],n,m,t;            //w是权值,b是兄弟,c是孩子
     8 
     9 inline int read()                                //读入优化
    10 {
    11     int f=1,x=0;
    12     char c=getchar();
    13     while (!isdigit(c))
    14         f=c=='-'?-1:1,c=getchar();
    15     while (isdigit(c))
    16         x=(x<<1)+(x<<3)+(c^48),c=getchar();
    17     return x*f;
    18 }
    19 
    20 void dfs(int root,int len)                            //记忆化
    21 {
    22     if (f[root][len]>=0)                            //出现过,直接返回
    23         return;
    24     if (root==0||len==0)                        //到边界了
    25     {
    26         f[root][len]=0;
    27         return;
    28     }
    29     int i;
    30     dfs(b[root],len);                            //兄弟和自己有同等的发展机遇
    31     for (i=0;i<len;i++)                            //兄弟孩子一起分
    32     {
    33         dfs(b[root],i);
    34         dfs(c[root],len-i-1);
    35     f[root][len]=max(f[root][len],max(f[b[root]][len],f[b[root]][i]+f[c[root]][len-i-1]+w[root]));
    36     }
    37     return;
    38 }
    39 
    40 int main()
    41 {
    42     int i,j;
    43     n=read();
    44     m=read();
    45     memset(f,-1,sizeof(f));                        //作死,初始化
    46     for (i=1;i<=n;i++)
    47     {
    48         t=read();
    49         w[i]=read();
    50         if (!t)
    51             t=n+1;
    52         b[i]=c[t];                                //模拟链表
    53         c[t]=i;
    54     }
    55     dfs(c[n+1],m);
    56     printf("%d",f[c[n+1]][m]);
    57     return 0;
    58 }

    总结一点,树规的dfs很好理解,从哪开始,从哪结束,从哪输出。

     

    嗯,就这样了。

  • 相关阅读:
    JavaScript你所不知道的困惑(3)
    Android的代码都得自己一个个敲一遍吗?
    现代化农业在美国的兴起与发展
    高内聚与低耦合实现小记
    iOS 载入图片选择imageNamed 方法还是 imageWithContentsOfFile?
    swift笔记——环境搭建及Hello,Swift!
    HDU 4832(DP+计数问题)
    [TJOI2019]甲苯先生的线段树
    2019-8-31-C#-如何写-DEBUG-输出
    2019-8-31-C#-如何写-DEBUG-输出
  • 原文地址:https://www.cnblogs.com/Ronald-MOK1426/p/8448397.html
Copyright © 2020-2023  润新知