Luogu-P2014 选课
题目
测试得分: 100
主要算法 : 树型DP、分组背包
题干:
树上分组DP
应试策略:
- 对题目分析,这可能是一片森林,所以用0结点将森林连接起来,构成一棵根结点为0的树
- 对于题目分析,是树上DP,但是对于每一个子树,只有一种选择,对每一棵子树又是分组背包
- 树型分组背包DP
代码
#include<vector> #include<stdio.h> #include<stdlib.h> #define FORa(i,s,e) for(int i=s;i<=e;i++) #define FORs(i,s,e) for(int i=s;i>=e;i--) using namespace std; const int N=1000,M=1000; int n,m,v[N+1],f[N+1][M+1]; //f[u][t]代表的以u为子树,选取t个课程的最多的学分 vector<int> son[N+1]; inline int max(int fa,int fb){return fa>fb?fa:fb;} void Dp(int u) { f[u][0]=0; for(int i=0;i<son[u].size();i++)//分组背包 { int v=son[u][i]; Dp(v); FORs(t,m,0)//零一背包 FORa(j,0,t) f[u][t]=max(f[u][t],f[u][j]+f[v][t-j]); } if(u) FORs(i,m,1) f[u][i]=f[u][i-1]+v[u]; } int main() { int from,dis; scanf("%d%d",&n,&m); FORa(i,1,n) scanf("%d%d",&from,&v[i]),son[from].push_back(i); Dp(0); printf("%d",f[0][m]); return 0; } /* 7 4 2 2 0 1 0 4 2 1 7 1 7 6 2 2 */
解析:
- 还是比较简单的
- 瞄一眼下去就是DP
- 但是怎么DP呢?
- 可以发现学习课程是有顺序的
- 我马上想到了DAG
- 然后又发现每门课有最多有一个先修课
- 所以这一定是一个森林
- 为了方便处理也是为了迎合输入数据
- 就把0号节点看做根节点即可
- 这样实质上就什么也不用处理
- 只需要在考虑的时候
- 把0号节点列入必选的范围即可
- 即要选m+1门课
- 解决了这个问题之后
- 我们就考虑怎么在树上DP就行了
- DP的核心是状态的设计和重叠的子问题结构
- 我们发现
- 树本身就是一个递归的结构
- 所以考虑在每棵子树上DP
- 然后合并就行了
- 怎么在子树上DP呐(ㆀ˘・з・˘)?
- 因为DP的一个核心思想是从最简单的子问题开始,逐步扩大子问题规模
- 所以我们当然要从最简单的状态讲起
- 一棵子树最简单的状态是啥?
- 对一棵子树,要想选其中的点,根节点是必须选的
- 所以不妨设根节点为1号节点
- 然后按我们存图的顺序依次给每个儿子编号(从2(到儿子的个数+1))
- 这样状态就好设计了
- 不妨设f[now][j][k]表示以now为根节点的子树
- 考虑前j个节点选k门课的方案数
- 因为1号节点是根节点,显然递推起点f[now][1][1]=val[now]
- 这样很容易得到状态转移方程
- f[now][j][k]=max(f[now][j-1][k],f[son][所有节点数][l]+f[now][j-1][k-l]);
- 然后我们观察等式两边的特点
- 哪些是我们已知的?
- 在对now求解前
- 我们至少已经处理完了前面的子树
- 所以f[son][所有节点数][l]
- 是可以直接用的
- 然后 在处理第j个节点前
- 前j-1个节点是我们已经处理过的
- 所以f[now][j-1][k]和f[now][j-1][k-l]也不用考虑循环顺序问题
- 但是问题来了
- 这样开三维数组不会炸空间吗
- 也许本题不会
- 但是我们可以很显然的发现
- 空间是可以优化的
- 只要稍稍改变循环顺序即可
- 我要用到j-1的内容
- 都是满足l<k的
- 所以倒着循环k
- 这样就可以使我们在一个数组中当前值和上面我们用到的值完全不影响
总结:
1.确定建立模型
2.构建知识架构,模型体系