题解 P2014 【选课】
HullEssien
2017-09-24 20:12:43
给树形DPの初学者(其实就是我哈哈(๑•̀ㅂ•́)و✧)
其实这个题不知道为什么是提高+/省选-
还是比较简单的
瞄一眼下去就是DP
但是怎么DP呢?
可以发现学习课程是有顺序的
我马上想到了DAG
然后又发现每门课有最多有一个先修课
所以这一定是一个森林
为了方便处理也是为了迎合输入数据
就把0号节点看做根节点即可
这样实质上就什么也不用处理
只需要在考虑的时候
把0号节点列入必选的范围即可
即要选m+1门课
(我看楼下这些问题都有讲,但是唯独状态转移方程是怎么推出来的没有讲,可能是觉得太简单了吧.但是DP最核心的部分就是推状态转移方程啊,而且这恰恰是新手最欠缺的地方,我觉得来看题解的人都是来看这个的吧(划掉))
解决了这个问题之后
我们就考虑怎么在树上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
这样就可以使我们在一个数组中当前值和上面我们用到的值完全不影响
就酱~
最后的状态转移方程在代码里
代码:
```cpp
#include<iostream>
#include<cstdio>
#define maxn 1000
using namespace std;
int n,m,f[maxn][maxn],head[maxn],cnt;
struct edge
{
int to,pre;
}e[maxn];
inline int in()
{
char a=getchar();
while(a<'0'||a>'9')
{
a=getchar();
}
int t=0;
while(a>='0'&&a<='9')
{
t=(t<<1)+(t<<3)+a-'0';
a=getchar();
}
return t;
}
void add(int from,int to)
{
e[++cnt].pre=head[from];
e[cnt].to=to;
head[from]=cnt;
}
void dp(int now)
{
// f[now][0]=0;
for(int i=head[now];i;i=e[i].pre)
{
int go=e[i].to;
dp(go);
for(int j=m+1;j>=1;j--)
{
for(int k=0;k<j;k++)
{
f[now][j]=max(f[now][j],f[go][k]+f[now][j-k]);
}
}
}
}
int main()
{
n=in(),m=in();
for(int i=1;i<=n;i++)
{
int fa=in();
f[i][1]=in();
add(fa,i);
}
dp(0);
printf("%d\n",f[0][m+1]);
return 0;
}
```