[CF735E]Ostap and Tree

题目

传送门 to luogu

思路

为了去重,我定义了三维的 d p \tt dp dp 数组 f ( x , i , j ) f(x,i,j) f(x,i,j) 表示,只考虑 x x x 子树,子树内最靠近 x x x 的点距离为 i i i ,子树外最靠近 x x x 的点距离为 j j j 。对于每组 x , i x,i x,i ,不同的 j j j 一定存储不同的方案。但是 i i i 不同时,可能存在相同方案。

我们的 d p \tt dp dp 数组只保证 x x x 子树内的点合法,对于子树外的,不要多管闲事。但是考虑子树外的点可以减少状态数——最近的祖先不会超过 2 k + 1 2k+1 2k+1 ,否则中间有个点够不到。最近的子树内的点不超过 2 k + 1 2k+1 2k+1 ,理由类似。事实上是小于 2 k + 1 2k+1 2k+1 ,否则 x x x 就需要是染色点,最近点就是自己,深度就成为了 0 0 0

定义初始状态时犯了难。因为子树内可能没有特殊点。可是子树外也可能没有,我们怎样解决的?认为其距离为 k + 1 k+1 k+1 。此处当然可以模仿,认为子树内的 2 k + 1 2k+1 2k+1 深度有一个染色点。因为这个深度只能代表没有点。

然后直接合并即可。复杂度 O ( n k 3 ) \mathcal O(nk^3) O(nk3)

代码

我的 k k k 加了 1 1 1 改成了严格小于号。

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
inline void writeint(long long x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) writeint(x/10);
	putchar((x%10)^48);
}
template < class T >
void getMax(T&a,T b){ if(a < b) a = b; }
template < class T >
void getMin(T&a,T b){ if(b < a) a = b; }

const int MaxN = 102, MaxK = 23;
struct Edge{
	int to, nxt;
	Edge(int T=0,int N=0){
		to = T, nxt = N;
	}
} edge[MaxN<<1];
int head[MaxN], cntEdge, n;
void addEdge(int a,int b){
	edge[cntEdge] = Edge(b,head[a]);
	head[a] = cntEdge ++;
	edge[cntEdge] = Edge(a,head[b]);
	head[b] = cntEdge ++;
}

const int Mod = 1e9+7;
int k; // 点到染色点的最近距离严格小于k
int tmp[MaxK<<1]; // dp转移临时数组
// 对于子树x,最近祖先点距离i(i>0),最近子树点距离j(j>=0)
int dp[MaxN][MaxK<<1][MaxK<<1];

void dfs(int x,int pre){
	for(int d=1; d<2*k; ++d)
		dp[x][d][0] = 1; // 自己是点
	for(int d=1; d<k; ++d)
		dp[x][d][2*k-1] = 1; // 没点
	for(int i=head[x],y; ~i; i=edge[i].nxt){
		if((y = edge[i].to) == pre) continue;
		dfs(y,x); // 递归处理
		// 要用到更小的d,我的代码中必须从大到小
		for(int d=2*k-1; d; --d){
			for(int j=0; j<2*k; ++j)
				tmp[j] = 0; // clear
			for(int p=0; p<2*k; ++p)
			for(int q=0; q<2*k; ++q){
				int dx = min(d,q+1);
				int dy = min(d+1,p+1);
				// 注意此处dy可能等于2k
				// 但是没用。一定不合法。
				if(d >= k && p >= k && q+1 >= k)
					continue; // 判断x合法性
				tmp[min(p,q+1)] += dp[y][dy][q]
					*1ll*dp[x][dx][p]%Mod;
				tmp[min(p,q+1)] %= Mod;
			}
			for(int j=0; j<2*k; ++j)
				dp[x][d][j] = tmp[j];
		}
	}
}

int main(){
	n = readint(), k = readint()+1;
	for(int i=1; i<=n; ++i)
		head[i] = -1;
	for(int i=1; i<n; ++i)
		addEdge(readint(),readint());
	dfs(1,-1); int ans = 0;
	for(int i=0; i<k; ++i)
		ans = (ans+dp[1][k][i])%Mod;
	printf("%d\n",ans);
	return 0;
}

别的

洛谷题解给出了 O ( n k 2 ) \mathcal O(nk^2) O(nk2) 的算法。算法本身不重要,但是它让我看到了另一种树形 d p \tt dp dp 。没必要拘泥于 整棵子树都合法。用状态中的一个维度来记录,哪些部分已经保证了合法。

题解中,用 f ( x , i ) f(x,i) f(x,i) 表示,对于子树 x x x ,与 x x x 的距离超过 i − k i-k ik 的都已经被搞定,但是 i − k − 1 i-k-1 ik1 距离的位置没有被搞定(“最近”染色点位于距离 i i i 处)。为什么“最近”要加引号?因为这里的最近不是所有点中最近的一个,而是 能够完整地解决比它更深的点 的黑点。

转移很简单, f ( x , i ) f(x,i) f(x,i) f ( y , j ) f(y,j) f(y,j) 合并:

  • i + j + 1 ≤ 2 k + 1 i+j+1\le 2k+1 i+j+12k+1 时,可以完整地解决,所以转移到 f [ x , min ⁡ ( i , j + 1 ) ] f[x,\min(i,j+1)] f[x,min(i,j+1)]
  • i + j + 1 > 2 k + 1 i+j+1>2k+1 i+j+1>2k+1 时,不可以完整地解决,所以只能转移到 f [ x , max ⁡ ( i , j + 1 ) ] f[x,\max(i,j+1)] f[x,max(i,j+1)]

这里的第二条是这样的感觉:

      x
     / \
    O   H
   /
  x
 /
H

k = 1 k=1 k=1 ,染色点为 H \tt H H 。看似最近点为右边的 H \tt H H ,实际上, O \tt O O 没有被覆盖,我们要记录左边的 H \tt H H 的深度来警醒子树外的点: O \tt O O 没有被搞定!

其实这张图是危险的化合物一氧化二氢。

初始化则类似, f ( x , k + 1 ) = 1 f(x,k+1)=1 f(x,k+1)=1 表示没有点, f ( x , 0 ) = 1 f(x,0)=1 f(x,0)=1 表示自己就是染色点。归纳法知不重复不遗漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值