【动态规划】之树形DP《收集果子》题解与各部分分附代码

问题描述

一共有 n n n 个果子分布在 n n n 个平台上,有的平台有许多个果子,有的平台没有果子。
n n n 个平台间由 n − 1 n - 1 n1 座桥连接。
Z Z Z 君从 1 1 1 号平台开始捡果子,最终君捡到了 n n n 个果子。
Z Z Z君在想,要是有些桥损坏了,我岂不是捡不到 n n n 个果子了?
于是君想让你算一算,在所有 2 n − 1 2^{n-1} 2n1种情况下(每个桥可以损坏或不
损坏), 君有多少种情况可以捡到k个果子。
为了避免 64 64 64 位整数输出,请将答案对 1 0 9 + 7 10^9+7 109+7 取。

输入格式

第一行两个整数 n , k n,k n,k
接下来一行 n n n 个整数,表示每个平台上果子的数量。
接下来 n − 1 n-1 n1 行,每行两个整数 x x x, y y y,表示平台连接情况。

输出格式

一行一个整数表示答案。

样例输入

3 1
1 1 1
1 2
2 3

样例输出

2

【样例解释】
第一个桥坏掉,第二个坏不坏都可以。
【数据规模和约定】
对于 30% 的数据,满足 n,k< 20。
对于 60% 的数据,满足 n,k< 100。
对于 100% 的数据,满足 n,k≤ 1000。


60 60 60 分的话其实很好理解,直接一个树上背包

f [ i ] [ j ] f[i][j] f[i][j] 表示以 i i i 为根的子树收集到 j j j 个果子的方案数

不选 k k k 号节点的话,它后面的子树节点都不选,一种方案都不剩,这样的话对答案的贡献就相当于是 f [ i ] [ j ] ∗ 2 ( s i z e [ k ] ) f[i][j] * 2^{(size[k])} f[i][j]2(size[k])
size[k]表示以 k k k 为根节点的子树的大小

选的话就也跟个 背包 一样

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
const int N=1e3+10;
int n,k;
ll a[N],b[N],c[N];
ll f[N][N],size[N];
int head[N],next[N],ver[N],tot;
void add(ll x,ll y)
{
	ver[++tot]=y;
	next[tot]=head[x];
	head[x]=tot;
}
void dfs(int x,int fa)
{
	size[x]=1;
	f[x][a[x]]=1;
	for(int i=head[x];i;i=next[i])
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs(y,x);
		size[x]+=size[y];
		for(int i=0;i<=k;i++)
		{
            b[i]=c[size[y]-1]*f[x][i];
            for(ll j=0;j<=i;j++)
            {
                b[i]+=f[x][i-j]*f[y][j] % mod;
            }
        }
        for(int i=0;i<=k;i++)
        {
            f[x][i]=b[i];
        }
    }
}
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
    }
    for(int i=1,x,y;i<=n-1;i++)
    {
        scanf("%d%d",&x,&y);
        add(x,y);
		add(y,x);
    }
    c[0]=1;
    for(int i=1;i<=n;i++)
	{
		c[i]=c[i-1]*2%mod;
	}
	dfs(1,-1);
    printf("%lld",f[1][k]);
    return 0;
}

30分写法就是在此基础上少取几次mod(我在大佬的30分代码上,多加几个取模就是上面的60分代码)

所以 yxc 说:少生孩子多取膜

30 30 30 分的就是给所有Bianca编个号,直接用二进制表示有哪些是断的,有哪些是没断的,然后对于每状态做 D F S DFS DFS

然后是我考试时候的代码(+后来写的注释):

//T5 树形dp,f[i][j]表示dp到第i个点收集j个果子的方案数,n三方(还是n方?)60或100
#include<bits/stdc++.h>
using namespace std;
#define N 1010
#define Mod 1000000007
#define int long long
int sz[N],f[N][N],hd[N],n,k,ver[N<<1],a[N],v[N],nxt[N<<1],tot,b[N];
void lian(int x,int y){ver[++tot]=y;nxt[tot]=hd[x];hd[x]=tot;}//正常的连边
void dp(int x,int y)
{
	sz[x]=1;
	for(int i=hd[x];i;i=nxt[i])
	{
		if(ver[i]==y)continue;
		for(int j=n;j>=v[ver[i]];j--)f[ver[i]][j]=f[x][j-v[ver[i]]];//感觉跟背包差不多的状态转移
		dp(ver[i],x);
		sz[x]+=sz[ver[i]];//求以x为根的子树节点大小
		for(int j=0;j<=n;j++)f[x][j]=((a[sz[ver[i]]-1]*f[x][j])%Mod+f[ver[i]][j])%Mod;
/*
从0开始循环,以x为根的子树下边的节点随便要不要都随便,
就有a[sz[ver[i]]-1]种方案,再乘之前所求的f[x][j]然后加上自己子节点的贡献,
记得多取膜
*/
	}
}
signed main()
{
	scanf("%lld%lld",&n,&k);int x,y;a[0]=1;
	for(int i=1;i<=n;i++)a[i]=(a[i-1]<<1)%Mod;//预处理出2的n次方
	for(int i=1;i<=n;i++)scanf("%lld",&v[i]);//输入每个点的权值
	f[1][v[1]]=1;//dp到第一个节点,取v[1]个苹果肯定有一种方案
	for(int i=1;i<n;i++){scanf("%lld%lld",&x,&y);lian(x,y);lian(y,x);}//读入+连边
	dp(1,0);cout<<f[1][k];//dp+输出
	return 0;
}
/*
3 1
1 1 1
1 2
2 3

2
*/

这不是 €€£ 跑的数据,所以 O ( N 3 ) O(N^3) O(N3) 是肯定过不了的,需要至少 O ( N 2 ) O(N^2) O(N2)左右的方法

然而在原来的做法中也很难去优化,所以我们可以换个思路,把状态变一下,用 f [ i ] [ j ] f[i][j] f[i][j] 表示 d p dp dp 到第 i i i 个点收集 j j j 个果子的方案数(乍一看没啥区别)

然后就是不断上下传递信息统计答案的过程

根据题意可知每个点的权值和应该是等于 n n n 的,所以比如jsy大佬给我的数据

5 6
1 3 4 5 2
1 2
2 3
4 1
2 5

抛开 n n n 的限制答案应该是 5 5 5

但由于该写法特殊,不能过这样的不合法情况


其实这个题目中点和边的数量给出来后我们很自然能看出这是一个树状的结构,而题目中又要求找最优解之类的东西,所以理所当然往 树形dp 上思考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值