题目链接:sicily 1019
题目分析:恶心的一笔的一道题,不过确实是一道好题。给你一棵树,n个结点,结点间距离为1,每个结点有一个值,第一次走过某个节点获得该结点的值,问用m步遍历树可以获得总值的最大值。仔细想想这道题其实这道题并不是特别难,只是之前没有做过类似的题目,所以一时半会儿下不了手。
解题思路:
树形DP,什么是树形dp呢?其实不用管那么多的啦,都是动态规划就对了,尤其是这道题,如果一直在想树的话,就永远不能发现它是一个背包了!
1、首先注意到这棵树只有是一棵树这个条件,没有其他的二叉之类的特殊条件,所以不能分成左右两棵子树的方法来做;
2、然后考虑做法,显然,树上的动态规划得用递归来做比较简单,不然还得树结点来个拓扑排序(太麻烦),所以直接递归即可,用vis数组记录结点是否被访问过;
3、然后来考虑状态,很容易想到就是用dp[i][j]表示以 i 为根花费 j 步可以获得的最大值,但是这样的想法很容易看穿有一个漏洞——走完了一个结点的左子树还可以返回然后走右子树啊!那么刚刚的状态就会WA,我们需要对状态进行修正——dp[i][j][0]表示以 i 为根花费 j 步并且不回到 i 结点可以获得的最大值 && dp[i][j][1]表示以 i 为根花费 j 步并且回到 i 结点可以获得的最大值;
4、状态考虑完了,那么来最难的部分——状态转移方程(本人之前做的树形DP比较少)。考虑当前结点index,然后它有很多棵子树,子树的结点保存在vec[index]中,假设现在index有len个子树,那么我们可以把他们想象成len个物品,我们要给每个物品分配若干步数使得总的步数最大,就变成背包问题了(int tmp = vec[index][i]):
我们遍历len个子树,第 i 步结束之后,我们就得到了考虑了vec[index][j](j ≤ i)这 i 个结点之后的所有步数的最大值,看看代码(非递归版的):
for(int i=0;i<len;i++)
{
int tmp=vec[index][i];
for(int j=m;j>=1;j--)
{
for(int k=1;k<=j;k++)
{
if(k>=2)
{
dp[index][j][0]=max(dp[index][j][0],
dp[index][j-k][0]+dp[tmp][k-2][1]);
dp[index][j][1]=max(dp[index][j][1],
dp[index][j-k][1]+dp[tmp][k-2][1]);
}
dp[index][j][0]=max(dp[index][j][0],
dp[index][j-k][1]+dp[tmp][k-1][0]);
}
}
}
有没有觉得很眼熟!这其实就是背包!
第一重循环不说;第二重循环从最大步数开始,逐步算出dp[index][j][0]和dp[index][j][1],怎么算的,就要靠第三重循环了;把 j-k 步给之前算过的所有结点(dp[index][j-k][1]/dp[index][j-k][0]都是只包含之前的子结点的最大值),然后剩下 k 步,分情况考虑:
(1)如果 k ≥ 2,那么可以走了子结点再走回来:
dp[index][j][0] = max(dp[index][j][0] , dp[index][j-k][0] + dp[tmp][k-2][1]);
dp[index][j][1] = max(dp[index][j][1] , dp[index][j-k][1] + dp[tmp][k-2][1]);
(2)必须要算的:
dp[index][j][0] = max(dp[index][j][0] , dp[index][j-k][1] + dp[tmp][k-1][0]);
这就是状态转移方程!
5、递归求解!
代码:(递归版的,懒得写拓扑排序)
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define MOD 100007
using namespace std;
int n,m,a[105],v[105],dp[105][205][2];
vector<int> vec[105];
void dfs(int index,int sum)
{
v[index]=1;
int len=vec[index].size();
if(!len)
for(int i=0;i<=m;i++)
dp[index][i][0]=dp[index][i][1]=a[index];
dp[index][1][0]=dp[index][1][1]=dp[index][0][0]=dp[index][0][1]=a[index];
for(int i=0;i<len;i++)
{
int tmp=vec[index][i];
if(v[tmp])
continue;
dfs(tmp,sum);
for(int j=sum;j>=1;j--)
{
for(int k=1;k<=j;k++)
{
if(k>=2)
{
dp[index][j][0]=max(dp[index][j][0],
dp[index][j-k][0]+dp[tmp][k-2][1]);
dp[index][j][1]=max(dp[index][j][1],
dp[index][j-k][1]+dp[tmp][k-2][1]);
}
dp[index][j][0]=max(dp[index][j][0],
dp[index][j-k][1]+dp[tmp][k-1][0]);
}
}
}
}
int main()
{
while(~scanf("%d %d",&n,&m))
{
memset(vec,0,sizeof(vec));
memset(dp,0,sizeof(dp));
memset(v,0,sizeof(v));
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d %d",&x,&y);
vec[x].push_back(y);
vec[y].push_back(x);
}
dfs(1,m);
printf("%d\n",max(dp[1][m][1],dp[1][m][0]));
}
return 0;
}
总结:
1、树上的动态规划!说白了其实也是背包问题,不要想太多~
2、用递归的思路会方便很多,但是有时可能会有爆栈等问题。
3、很神奇的一种题,蛮好玩的。