当先锋百科网

首页 1 2 3 4 5 6 7

在这里插入图片描述


题目大意:树上有 m 个关键点,选出一部分点,使得任意一对点间的距离不大于 k k k,问有多少种选法。

容易想到构造状态 d p [ u ] [ d e p ] dp[u][dep] dp[u][dep] 表示在 u u u 结点为根结点的子树中选的结点深度不大于 d e p dep dep 的方案总数。

这题的状态构造方法类似于 https://codeforc.es/contest/1249/problem/F

不过这题是计数类的DP,计数类的DP比较难处理,需要将情况不重复不遗漏的统计显然更难了,尤其是树上计数的DP

直接暴力转移的复杂度为 O ( n 2 ) O(n^2) O(n2),用前缀和优化一下,转移复杂度可以降低到 O ( n ) O(n) O(n)。但会出现重复的情况,用容斥去除即可。

关于统计答案,显然也会出现重复的情况,可以按深度来划分统计。
详见代码


代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int maxn = 5e3 + 10;
vector<int> g[maxn];
ll dp[maxn][maxn];
ll sum[maxn][maxn];
ll f[maxn],ans = 0;
bool is[maxn];
int n,m,k;
void dfs(int u,int fa) {
	// 为了更好转移,前缀和用另外一个数组sum[u][dep],dep[u][dep]表示最深的点的深度为 dep 的方案
	// sum[u][dep] 则表示最深的深度不大于 dep 的方案
	for(auto it : g[u]) {
		if(it == fa) continue;
		dfs(it,u);
		for(int i = 1; i <= k - 1; i++) {		//f[i] 用来统计考虑当前子树的答案
			f[i] = dp[u][i] * sum[it][min(i,k - i) - 1] % mod;
			f[i] += dp[it][i - 1] * sum[u][min(i,k - i)] % mod;
			f[i] %= mod;
		}
		for(int i = 1; i <= k / 2; i++)			//容斥去除重复的
			f[i] = (f[i] + mod - dp[u][i] * dp[it][i - 1] % mod) % mod;
			
		for(int i = 1; i <= k; i++) {
			//加上考虑了当前这一棵子树的答案
			dp[u][i] += dp[it][i - 1] + f[i];
			dp[u][i] %= mod;
			sum[u][i] = (sum[u][i - 1] + dp[u][i]) % mod;
		}
	}
	if(is[u]) {	
		//若根节点点可选,dp[u][i]要乘2,因为根节点有选和不选两种情况
		sum[u][0] = dp[u][0] = 1; //一个点都不选不能算一种方案,因此初始化1,前面的更新同理,因此不会重复
		for(int i = 1; i <= k; i++) {
			dp[u][i] = dp[u][i] * 2 % mod;
			sum[u][i] = (sum[u][i - 1] + dp[u][i]) % mod;
		}
	}
	if(u == 1) ans = (ans + sum[u][k]) % mod;	//当u 是根结点时,统计深度为 k 以内的所有方案
	else ans = (ans + dp[u][k]) % mod;			//否则,只统计深度为 k 的答案
	//相当于对深度大于 k 的情况,按深度子树根节点和深度 k + 1,k + 2 分开统计,这样一定不会重复
}
int main() {
	scanf("%d%d%d",&n,&m,&k);
	for(int i = 1; i < n; i++) {
		int u,v;scanf("%d%d",&u,&v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int i = 1; i <= m; i++) {
		int x;scanf("%d",&x);
		is[x] = true;
	}
	dfs(1,0);
	printf("%lld\n",ans);
	return 0;
}