树形DP即在树上的DP
主要是把儿子的最优解传给父亲
与普通DP一样,也分几步
1.确定状态->第一维是节点,根据实际情况考虑加不加维
2.状态转移
3.边界条件->边界一般是叶子节点吧
4.答案dp[根][...]
以下部分来源于大佬Faithfully_lyl的博客
类型1:分左右两个儿子情况
二叉苹果树
有一棵苹果树,它是一棵二叉树,共N个节点,树节点编号为1~N,编号为1的节点为树的根,边可理解为树的分枝,每个分支都长着若干个苹果,现在要减去若干个分支,保留M个分支,使得这M个分支中的苹果数量最多。
样例输入:
5 2
1 3 1
1 4 10
2 3 20
3 5 20
样例输出
21
分析
我们发现,可以分为以下3种情况
1.树根的左子树为空,全部保留右子树,右子树中保留j-1
个结点;
2.树根的右子树为空,全部保留左子树,左子树中保留j-1个
结点;
3.树根的两棵子树非空,设左子树保留k个结点,则右子树
保留j-k-1个结点。
我们取这3种情况的最大值
保留k条边就是k+1个节点
于是定义状态f[节点][呃...]
经过我多年的观察和考证
一般第一行有几个变量,状态就是几维(数位dp除外),例如本题,状态定义应该还有一个保留节点的个数
于是答案就是f[1][k+1]
在例如背包,第一行一个总容量,一个总物品量,于是答案就是f[总容量][种物品量]
而这道题,确实这两个变量就足够了
转移方程很容易写出
f[i,j]=max{f[ch[i,0],k]+f[ch[i,1],j-k-1]+a[i]}(0<=k<=j-1)
边界条件,若为叶子节点,f[i][j]=a[i]
#include<bits/stdc++.h>
#define N 105
using namespace std;
int child[N][2],father[N],a[N],f[N][N];
int first[N],next[N],to[N],w[N],tot;
int n,m;
void add(int x,int y,int z)
{
next[++tot]=first[x];
first[x]=tot;
to[tot]=y;
w[tot]=z;
}
void dfs(int cur,int fa)
{
for(int i=first[cur];i;i=next[i]){
int t=to[i];
if(t==fa) continue;
a[t]=w[i],father[t]=cur;
if(child[cur][0]) child[cur][1]=t;
else child[cur][0]=t;
dfs(t,cur);
}
}
int dp(int i,int j)
{
if(i==0||j==0) return 0;
if(f[i][j]) return f[i][j];
if(!child[i][0]) return a[i];
for(int k=0;k<=j-1;k++)
f[i][j]=max(f[i][j],dp(child[i][0],k)+dp(child[i][1],j-k-1)+a[i]);
return f[i][j];
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n-1;i++){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
dfs(1,0);
cout<<dp(1,m+1);
return 0;
}
类型二:普通树上的dp
普通树和二叉树的区别是啥子嘞,
就是说二叉树里一个结点下最多只有两个儿子结点,所以状态比较好转移(从儿子向上走)。但普通树就不止两个了,可能有多个,那就不好玩了╭(╯^╰)╮。可是,我们可以把普通树转成二叉树啊,一样的dp,熟悉的感觉~
转化的口诀(原则):左儿子,右兄弟
就是说当前结点的第一个儿子,放在它的左子树,其余的儿子挂在第一个儿子的右子树上。
那么对于每个结点而言,它左边的才是它真正的儿子,右边都是它兄弟(同一个父亲)
(大家凑合看看,然后自己试着转转)
如何检验是否转对了?
1.每个结点的信息没有改变(比如其父亲结点,儿子结点,还有的就因题而异了)
2.可以从二叉树再转回去(基本上这条如果做到了,就没什么大问题了)
普通树已经转成二叉树了,接下来就好办多了
代码
for(int i=1;i<=n;i++){
int fa=father[i];
if(!son[fa]) child[fa][0]=i;//如果父亲还没有儿子,他的左儿子是i
else child[son[fa]][1]=i;//否则他的其他儿子接在他儿子的右儿子上
son[fa]=i;//更新他的最后一个儿子
}
例题
选课
题目描述
学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。学校开设了 N(N<300)门的选修课程,每个学生可选课程的数量 M 是给定的。学生选修了这M门课并考核通过就能获得相应的学分。
在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如《Frontpage》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Frontpage》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。每门课都有一个课号,依次为1,2,3,…。 例如:
课号 | 先修课号 | 学分 |
1 | 无 | 1 |
2 | 1 | 1 |
3 | 2 | 3 |
4 | 无 | 3 |
5 | 2 | 4 |
表中 1 是 2 的先修课,2 是 3、4 的先修课。如果要选 3,那么 1 和 2 都一定已被选修过。
你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。
输入格式
输入文件的第一行包括两个整数 N、M(中间用一个空格隔开)其中 1≤N≤300,1≤M≤N。
以下N行每行代表一门课。课号依次为 1,2,…,N。每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。学分是不超过 10 的正整数。
输出格式
输出文件只有一个数,实际所选课程的学分总数。
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
样例输出
13
“每门课的直接先修课最多只有一门。”这句话告诉我们每门科只会对应一个父亲
“两门课也可能存在相同的先修课。"这是在说明这是一棵普通树,每个节点下不一定只有两个儿子
这种无先修课的就是根节点啦,而无先修课的课不止一门,所以这是一个森林~(但和普通树转二叉树没什么区别,直接选中一棵树的根节点作为森林的根节点,然后左儿子,右兄弟)
然后对于每个节点而言,如果选他的左儿子,那么该节点必选,但如果选他的右儿子,就不一定要选根了(因为他们是兄弟关系呗)根据这个来状态转移,就和上一题类似了
#include<bits/stdc++.h>
#define N 305
using namespace std;
int n,m,child[N][2],father[N],son[N],f[N][N];
int first[N],next[N],to[N],tot,a[N];
bool bj=false;
void add(int x,int y)
{
next[++tot]=first[x];
first[x]=tot;
to[tot]=y;
}
void dfs(int cur,int fa)
{
for(int i=first[cur];i;i=next[i]){
int t=to[i];
if(t==fa) continue;
father[t]=cur;
dfs(t,cur);
}
}
int dp(int i,int j)
{
if(f[i][j]) return f[i][j];
if((i==0&&bj)||j==0) return 0;
if(!child[i][0]&&!child[i][1]) return a[i];
bj=true;
if(child[i][1]) f[i][j]=dp(child[i][1],j);
for(int k=0;k<=j-1;k++)
f[i][j]=max(f[i][j],dp(child[i][0],k)+dp(child[i][1],j-k-1)+a[i]);
return f[i][j];
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
int y,z;
cin>>y>>z;
a[i]=z,add(y,i);
}
dfs(0,-1);
for(int i=1;i<=n;i++){
int fa=father[i];
if(!son[fa]) child[fa][0]=i;
else child[son[fa]][1]=i;
son[fa]=i;
}
cout<<dp(0,m+1);
return 0;
}
类型3:树的直径
描述
树的直径,即这棵树中距离最远的两个结点的距离。每两个相邻的结点的距离为1,即父亲结点与儿子结点或儿子结点与父子结点之间的距离为1.有趣的是,从树的任意一个结点a出发,走到距离最远的结点b,再从结点b出发,能够走的最远距离,就是树的直径。树中相邻两个结点的距离为1。你的任务是:给定一棵树,求这棵树中距离最远的两个结点的距离。
输入
输入共n行 第一行是一个正整数n,表示这棵树的结点数 接下来的n-1行,每行三个正整数a,b,w。表示结点a和结点b之间有一条边,长度为w 数据保证一定是一棵树,不必判错。
输出
输出共一行 第一行仅一个数,表示这棵树的最远距离
样例输入
4
1 2 10
1 3 12
1 4 15
样例输出
27
100%的数据满足1<=n<=10000 1<=a,b<=n 1<=w<=10000
还是和我们之前一样,分析每一个点,发现最长链与它的关系那么我们状态就好定义了,而且我们也知道,某子树的直径是由它的最长链和次长链组成的。那么我们就用d1[i]表示以i为根的子树中最长链的长度,d2[i]表示以i为根的子树中次长链的长度。而对于1号节点而言,最长链要么经过它,要么不经过它。但反正求的ans总是取的max,所以不用单独弄出来搞
#include<bits/stdc++.h>
#define N 10005
using namespace std;
int n,a,b,w;
int ans=-1;
struct node{int v,len;};
vector<node> f[N];
int l1[N],l2[N];
void dfs(int u,int fa){
int i,j,k;
for(i=0;i<f[u].size();++i){//从0开始!!!
int v=f[u][i].v,len=f[u][i].len;
if(v==fa) continue;
dfs(v,u);
if(l1[v]+len>l1[u]) l2[u]=l1[u],l1[u]=l1[v]+len;
else if(l1[v]+len>l2[u]) l2[u]=l1[v]+len;
}
ans=max(ans,l1[u]+l2[u]);
}
int main(){
scanf("%d",&n);
int i,j,k;
for(i=1;i<n;++i)
{
scanf("%d%d%d",&a,&b,&w);
f[a].push_back((node){b,w});
f[b].push_back((node){a,w});
}
dfs(1,0);
printf("%d",ans);
return 0;
}