搜集钻石(树形DP经典例题)

搜集钻石

Description

 

蒜国有 n 座城市,编号从 1 到 n,城市间有 n−1 条道路,且保证任意两座城市之间是连通的。每一座城市有一定数量的钻石。

蒜头君想在蒜国搜集钻石。他从城市 1 出发,每天他可以通过城市之间道路开车到另外的城市。当蒜头第一次到一个城市的时候,他可以搜集完这个城市的所有钻石,如果他后面再来到这个城市,就没有砖石可以收集了。

蒜头君只有 K 天时间,请你帮算蒜头君计算他最多可以搜集多少钻石。

Input

 

第一行输入三个整数 n(1≤n≤100),K(0≤k≤200),表示一共有 n 座城市,蒜头君有 K 天时间。

接下里一行输入 n 个用空格隔开的整数,表示每个城市的钻石数量。每个城市的钻石数量不大于 1000。

接下来输入 n−1 行,每行输入两个整数 a(1≤a≤n),b(1≤b≤n),表示城市 a和城市 b 之间存在一条双向道路。

Output

 

输出一行一个整数表示蒜头君最大能获取的钻石数量。

Sample Input 1

3 2
3 8 3
1 3
3 2

Sample Output 1

14

Sample Input 2

6 2
5 9 8 4 9 2
1 6
6 2
2 5
5 3
5 4

Sample Output 2

16

Source

计蒜客

题意分析:有n个节点,n-1条边,联通,很容易就可以想到是一棵树,又让求最大值,自然而然的就联想到了树形DP;则原题就变成了——给你一棵树,每条边(双向)的权值为一,每个节点有一定价值且只能取一次(注意去了一定还要回来才可以到同层的另一子树),有看k天,问能取到的最大价值;

题目分析:对于每个子树,它的子树可以去到不止一个(在时间够的情况下),且对于每棵子树有两种操作——去了回来和去了不会;去了回来的情况比较好考虑——如果根节点去了要回,则它的每个子树去了都要回,于是很容易就可以敲出一个树上的分组背包(具体操作不赘述);但麻烦的在不回来的情况——可以有很多个回来的字树,但只能有一个不回来的子树,根据惯性思维,在想出了多重背包解回来情况之后,便会在想到确定一个不回来的子树,再在剩下的子树中做一次分组背包,取最大值,我就收这种垃圾思维的影响苦苦肝了很久(欲哭无泪),接下来先送上我的70分代码(其实细节处理好了应该可以满分):

#include<bits/stdc++.h>
using namespace std;
int n,m,ma[1100],dp[1100][2100][2];
vector<int> vec[1100];
void dfs(int a,int b,int c,int fa)
{//cout<<a<<" "<<b<<" "<<c<<" "<<fa<<endl;
	int l=vec[a].size();
	if(b==0||l==1&&a!=1)//边界值
	{
		for(int i=0;i<=b;i++)
		{
			dp[a][i][c]=ma[a];
		}
		//cout<<a<<" "<<b<<" "<<c<<" "<<dp[a][b][c]<<endl;
	}
	else if(c==1)//要回来
	{
		int tmp=b-b%2,dp2[2100];
		memset(dp2,0,sizeof(dp2));
		for(int i=0;i<l;i++)//分组背包
		{
			if(vec[a][i]==fa) continue;
			dfs(vec[a][i],tmp-2,1,a);
			for(int j=tmp-2;j>=2;j-=2)
			{
				for(int e=0;e<=j;e+=2)
				{
					dp2[j]=max(dp2[j],dp2[j-e]+dp[vec[a][i]][e][1]);
					dp2[j+1]=dp2[j];
					//cout<<j<<dp2[j]<<endl;
				}
			}
		}
		for(int i=2;i<=b;i++)
		{
			dp[a][i][1]=dp2[i-2]+ma[a];
			//cout<<a<<" "<<i<<" "<<c<<" "<<dp[a][i][c]<<endl;
		}
		dp[a][0][1]=ma[a];
		dp[a][1][1]=ma[a];
		//cout<<a<<" "<<ma[a]<<endl;
		
	}
	else//不回来
	{
		dp[a][0][0]=ma[a];
		for(int k=0;k<l;k++)
		{
			if(vec[a][k]==fa) continue;
			dfs(vec[a][k],b-1,0,a);
			int tmp=b-b%2,dp2[2100];
			memset(dp2,0,sizeof(dp2));
			for(int i=0;i<l;i++)
			{
				if(vec[a][i]==fa||i==k) continue;
				for(int j=tmp-2;j>=2;j-=2)
				{
					for(int e=0;e<=j;e+=2)
					{
						dp2[j]=max(dp2[j],dp2[j-e]+dp[vec[a][i]][e][1]);
						dp2[j+1]=dp2[j];
					}
				}
			}
			for(int i=1;i<=b;i++)
			{
				for(int j=1;j<=i-1;j++)	
				{//cout<<a<<" "<<i<<" "<<c<<" "<<dp[a][i][c]<<" "<<dp[vec[a][k]][j][0]<<" "<<dp2[i-j]<<" "<<ma[a]<<endl;
					dp[a][i][0]=max(dp[a][i][0],dp[vec[a][k]][j-1][0]+dp2[i-j-2]+ma[a]);
					
				}
			}
		}
		//
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>ma[i];
	}
	for(int i=1;i<n;i++)
	{
		int a,b;
		cin>>a>>b;
		vec[a].push_back(b);
		vec[b].push_back(a);
	}
	dfs(1,m,1,-2);
	dfs(1,m,0,-2);
	cout<<dp[1][m][0];
}

有点长(还没过......);

接下来说一说正解思路(很简洁):首先当然要赋初值dp[u][a][b]统统赋成u节点的值,然后开始枚举子节点(注意不要枚举到父节点了),再递归到下一层的dp[v][a][b](没有它解决不了大问题,具体过程不要深想),再枚举0->m(m为最大天数),再枚举给前面已打出的量的天数e(0->j-1),前面已打出的和当前的又只能有一个去了不回来,于是不难列出方程:要回来的情况:dp[u][j][1]=max(dp[u][j][1],dp[u][e][1]+dp[v][j-e-2][1]); 不回来的情况:dp[u][j][0]=max(dp[u][j][0],dp[u][e][0]+dp[v][j-e-2][1]);dp[u][j][0]=max(dp[u][j][0],dp[u][e][1]+dp[v][j-e-1][0]);

AC代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,dp[210][210][2],ma[210];
vector<int> vec[210];
void dfs(int fa,int u)
{
	int l=vec[u].size();
	for(int i=0;i<=m;i++)
	{
		dp[u][i][1]=dp[u][i][0]=ma[u];
	}
	for(int i=0;i<l;i++)
	{
		int v=vec[u][i];
		if(v==fa) continue;
		dfs(u,v);
		for(int j=m;j>=1;j--)
		{
			for(int e=0;e<=j-1;e++)
			{
				if(j-e>=2)
				{
					dp[u][j][1]=max(dp[u][j][1],dp[u][e][1]+dp[v][j-e-2][1]);
					dp[u][j][0]=max(dp[u][j][0],dp[u][e][0]+dp[v][j-e-2][1]);//前面的不回来
				}
				dp[u][j][0]=max(dp[u][j][0],dp[u][e][1]+dp[v][j-e-1][0]);//后面的不回来
			}
		}
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>ma[i];
	}
	for(int i=1;i<n;i++)
	{
		int a,b;
		cin>>a>>b;
		vec[a].push_back(b);
		vec[b].push_back(a);
	}
	dfs(-1,1);
	cout<<dp[1][m][0];
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值