在朱全民的PPT介绍的一个树型DP经典题,《选课》,中文题目,不结束
找了很久找到了可以提交的OJ,重庆八中 http://www.cqoi.net:2012/JudgeOnline/problem.php?id=1376
简单分析一下:
1.建树,不要用一般的孩子表示法,这里要讲森林转为二叉树处理才能强劲有力,所以用(左)孩子(右)兄弟法建树
2.建树之后就可以DP,DP的策略写在代码中了,不多说
对于树型DP的初步感觉
1.建树很重要(跟图论里面构图很重要,构图失败基本上整个算法失败了)
2.树这种结构比较特殊,要找到特殊型入手,一是从属关系,即孩子是属于某个双亲的,二是平行关系,兄弟之间是平行的。因而一个节点的信息往往可以由其孩子的信息得到。同样的,一个节点的信息也有可能传递给孩子
3.唯一性:一个结点可能有多个孩子,但是双亲一定是唯一的,很多问题的突破就在此
/* 用孩子兄弟法建树 1.对于树的一个节点,分三个域,数据域,第一个孩子域,第一个右兄弟域 2.接下来每一行读两个信息,j,s。在第i行中,j表示i的先修课程,也就是i是j的孩子 先把j之前的第一个孩子保存下来,为k,可知i和k是兄弟关系,所以k的第一个右兄弟更新为i 同时i更新为j的第一个孩子 更新第一个孩子和第一个右兄弟都用数组来模拟链表实现 建树后得到的是森林,有一些元素是森林里面的树根,还要设置一个虚拟的节点来作为这些树根的双亲, 所以建树过程要记录哪些元素是树根,不过还好,输入中已经有了提示,不用另外记录。输入中,树根的双亲都定为0 这刚好符合我们的要求,我们就把虚拟的节点设为0,同样用孩子兄弟法将森林合并为一棵树 */ #include <cstdio> #include <cstring> #define N 310 #define M 310 #define INF 0x3f3f3f3f #define max(a,b) a>b?a:b int fc[N],rb[N],data[N]; //第一个孩子,第一个右兄弟,数据域 int dp[N][M]; //dp[i][j]以i为根,选j门课的最大值 int n,m; void build() { memset(fc,0,sizeof(fc)); memset(rb,0,sizeof(rb)); data[0]=0; for(int i=1; i<=n; i++) { int j,k; scanf("%d%d",&j,&data[i]); k=fc[j]; rb[i]=k; fc[j]=i; } } int dfs(int i ,int j) { if(i==0 || j==0) return dp[i][j]=0; if(dp[i][j]!=-1) return dp[i][j]; /* dp[i][j]表示以i为根的子树要选择j个课程的最大值(此时的树不是原始的森林而是孩子兄弟法表示的二叉树) dp策略为:对于当前节点,其左指针是它的第1个孩子,右指针是它的第一个兄弟,左边是从属关系,右边是平行关系 如果不选当前节点,那么它整个左子树都不能选了,因为整个左子树都是它的后代, 那么将由它的右子树分担选j个课程的责任(左边从属关系,右边平行关系,不受当前节点影响) 如果选了当前节点,那么剩下的课程就是j-1个,这时候即可以到左子树继续选,也可以到右子树继续选 且要满足两棵子树选课之和为j-1 */ dp[i][j]=0; int c=fc[i] , b=rb[i]; //选当前节点,那么孩子和兄弟都要考虑 for(int k=0; k<j; k++) //枚举左边分担的个数 { dfs(c,k); dfs(b,j-1-k); dp[i][j] = max(dp[i][j] , data[i]+dp[c][k]+dp[b][j-1-k]); } //不选当前节点,那么只能从它的兄弟开始选,但是它必须得有兄弟 dfs(b,j); dp[i][j] = max(dp[i][j] , dp[b][j]); return dp[i][j]; } void solve() { memset(dp,-1,sizeof(dp)); dfs(fc[0],m); printf("%d\n",dp[fc[0]][m]); } int main() { while(scanf("%d%d",&n,&m)!=EOF) { build(); //建树 solve(); } return 0; }