前言
学OI的时候就做过树形dp的题,不过那时候全在划水。看了看题解还不太懂就直接照着题解写了,现在再回来看还是不会QAQ,所以就再看看然后自己写了一遍。
A. 没有上司的舞会
题意
有 $N$ 个职员被邀请去参加公司的舞会,他们每个人对应着一个快乐指数 $h_i$ ,如果该职员来了就会为舞会增加$h_i$ 点快乐指数。这 $N$ 个职员之间有从属关系,也就是说他们的关系就像一棵以顶级上司为根的树,父结点就是子结点的直接上司。如果一个职员的直接上司来到了舞会,那么他本人就不会再来。问邀请哪些职员可获得最大的快乐指数,最大为多少。
思路
职员之间的关系是以树的形式给出的,所以先用链式前向星来存储数。存的时候我们用vis数组记录一下如何那个人有上司,就将他标记一下,最终没有标记的就是没有上司的,也就是顶级上司,也就是我们的根节点root。 树形dp,顾名思义,在树上进行dp,通过递归dfs,先算出子树的状态,再通过递归的回溯来合并。那么我们考虑一下设计状态,很明显一个人的状态有来和不来,两个情况。所以我们设计状态 $dp[i][0/1]$ 来表示职员 i 来或者不来,我们用u来表示当前节点,用v来表示当前节点的子节点,那么状态转移如下:
$dp[u][0]=max(dp[v][0],dp[v][1]) $ (上司u没有来,那么下属v可以来,也可以不来,选一个大的策略)
$dp[u][1]=dp[v][0]+h[u]$ (上司u来了,下属v肯定不来)
最终的答案就是 $max(dp[root][0],dp[root][1])$ ,上司来和不来两种策略中的最大一种。
代码实现
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
using namespace std;
const int maxn=6005;
int head[maxn];
struct edge
{
int next;
int to;
}e[maxn];
int cnt,n,l,k,vis[maxn],root;
int r[maxn];
int dp[maxn][2];
void add(int l,int k)
{
e[++cnt].next=head[l];
e[cnt].to=k;
head[l]=cnt;
}
void dfs(int u)
{
dp[u][0]=0;
dp[u][1]=r[u];
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
dfs(v);
dp[u][1]+=dp[v][0];
dp[u][0]+=max(dp[v][1],dp[v][0]);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&r[i]);
for(int i=1;i<n;i++)
{
scanf("%d%d",&l,&k);
add(k,l);
vis[l]++;
}
scanf("%d%d",&l,&k);
for(int i=1;i<=n;i++)
{
if(!vis[i]) root=i;
}
dfs(root);
printf("%d",max(dp[root][0],dp[root][1]));
return 0;
}
B. 战略游戏
题意
小姜老师要建立一个树,树节点两两之间通过道路连接,他要在这些节点上放一些小人,每个小人可监管与该节点相连的道路,问最少放置多少小人可以监管所有道路。(数据保证0为根节点)
思路
这个题跟上个题有点像,首先我们也是用链式前向星把树给存下来,不过这个题的输入跟上道题有点不一样,本质都是一样的。因为我们只需要dfs下去,然后向上回溯的时候合并,所以只需要存单向边即可。 对于一个节点我们也是有两种策略,选或者不选,那么我们也可以把状态写成 $dp[i][1/0]$ ,对应着 i 这个节点选或者不选,我们把当前节点看作u,子节点看作v,状态转移如下:
$dp[u][1]+=\Sigma (dp[v][0],dp[v][1]) +1$ (u选了,v选不选都可以,加上自身的1)
$dp[u][0]+=\Sigma dp[v][1]$ (如果u没有选,那么v一定要选,才能监管到u的道路)
很显然这个答案是 $min(dp[0][0],dp[0][1])$
代码实现
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
using namespace std;
const int maxn=1505;
struct edge
{
int next,to;
}e[maxn];
int u,v;
int head[maxn];
int dp[maxn][2];
int num;
int n,cnt;
void add(int u,int v)
{
e[++cnt].next=head[u];
e[cnt].to=v;
head[u]=cnt;
}
void dfs(int u)
{
dp[u][1]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
dfs(v);
dp[u][1]+=min(dp[v][1],dp[v][0]);
dp[u][0]+=dp[v][1];
}
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d%d",&u,&num);
for(int i=1;i<=num;i++)
{
scanf("%d",&v);
add(u,v);
}
}
dfs(0);
printf("%d",min(dp[0][0],dp[0][1]));
return 0;
}
C. 二叉苹果树
题意
有一颗二叉苹果树,也就是说树枝如果分叉,一定是分二叉。苹果树有 $n$ 个节点,树根编号为 1 。每个树枝上都有一定数量的苹果,如果最终保留 $q$ 根树枝,问最多能够保留多少苹果。
思路
这个树形dp要比上面的还要难一些,因为我们的状态不能够只考虑是否保留当前的树枝了,因为保留当前树枝也会有很多种关于保留子树枝选择。看了题解大佬的说法是,可以定义成 $f[u][j]$ 来表示 u 的子树上保留 j 条边时所能获得最大的苹果数。所以我们考虑一下转移方程,首先我们能保留的最大边数是当前子树所具有的所有树枝,对于一个节点下的两个树枝,我们可以选择取,或者不取,那么这就类似于一个背包问题,对于一定的容量(一定量的树枝),我们取几个树枝才能获得最大的价值。但是我们注意,对于一个节点u,我们如果要取他的节点v,那么我们u-v之间这个树枝一定要保留,不然会不能取得v,这是一个隐藏的条件。 因此转移方程: $dp[u][i]=max(dp[u][i],dp[u][i-j-1]+dp[v][j]+w[u][v] $ ,$w[u][v]$为当前u-v树枝的苹果数。代码里面因为我懒得计算子树的树枝数了,所以就直接从最大的q来枚举了,就没考虑那么多,可能复杂度稍微高一点,但是高不到哪去其实QAQ…
代码实现
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
using namespace std;
const int maxn=105;
struct edge
{
int next,to,w;
}e[maxn];
int n,cnt,q;
int u,v,w;
int dp[maxn][maxn];
int head[maxn];
void add(int u,int v,int w)
{
e[++cnt].next=head[u];
e[cnt].to=v;
e[cnt].w=w;
head[u]=cnt;
}
void dfs(int u)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
dfs(v);
for(int j=q;j>=0;j--)
{
for(int k=0;k<=j-1;k++)
{
dp[u][j]=max(dp[u][j],dp[u][j-1-k]+dp[v][k]+e[i].w);
}
}
}
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
dfs(1);
printf("%d",dp[1][q]);
return 0;
}