题目大意:树上有 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;
}