P9479 [NOI 2023] 桂花树

发布时间 2023-10-15 16:56:30作者: Natori

P9479 [NOI 2023] 桂花树

好题!

可以先看看这个,虽然感觉并没有什么用(


先考虑第一条限制。

在纸上画几个图,大概可以分成以下几类(左边是原树,右边是有 \(n+m\) 个点的新树):

黑色点表示原树上的点,蓝色点表示新加入的点。

上面三个图分别代表:新点挂在原树的一个点上成为叶节点(不一定挂在原树的叶节点上)、新点加在原树的一条边上、新点成为某两个原树上的点的 LCA。

不难发现只有前两种情况是满足要求的,原因显然。

那么我们就得到了第一条限制的等价描述:

新树上,新点在原树的若干条边上,或者新点的度数为 \(1\)

事实上,@Alex_Wei 在他的题解中有一个更厉害的描述:

新树上编号在 \([1,n]\) 中点的虚树等于原树。

我们不妨沿用 Alex_Wei 的描述, 从虚树的角度考虑第二个条件:

对于一个编号为 \(u\in[1,n+m]\) 的点,新树上编号前缀 \([1,u]\) 对应点集的虚树中,所有点编号均不超过 \(u+k\)

现在回到问题,从第二条限制出发,使用 增量法,在已求出虚树一个编号前缀点集的基础上考虑加入一个点带来的影响。

\(m=1\) 时,只有两种增加这个新点的方案:挂在节点上和加在边上。因此加点方案数是 \(num+num-1=num\times2-1\),其中 \(num\) 是虚树的点数,直接输出即可。

\(m\ge2\) 时,如果 \(k=0\) ,则仍只有上面的两种方式,答案显然。

否则 \(k>0\)

假设现在要向 \([1,u]\) 加入编号为 \(u+1\) 的点,此时仍然可以使用之前的两种方式,总方案数为 \(num\times2-1\)

但是现在还有一种方式是,新点作为编号在 \([u+1,u+k]\) 中的点加入点集。我们把这样的新点称为 变点。注意到变点的变化范围只有 \(k\),而 \(k\) 很小,所以考虑 状态压缩 DP:设 \(dp_{u,s}\) 表示考虑到编号 \(u\),已经选择变点的集合为 \(s\) 时,符合要求的树的个数。不难发现此时 \(num=n+u+\text{popcount}(s)\)

对于上面的这种方式,可以枚举 \(s\) 的每一位,遇到一个 \(1\) 就转移一次。

此外,由于普通点和变点显然可以同时存在,因此还有一种方式是同时加入一个普通点 \(u+1\) 和一个变点,如下图所示:

其中虚线点是变点,实线点是普通点。

不难发现此时变点可以选择的位置有 \(num-1\) 个,所以方案数是 \(num-1\)。注意此时对 DP 状态中的 \(s\) 也有影响。

以上转移具体见代码。

枚举 \(u,s\)\(s\) 的每一位,时间复杂度 \(\mathcal{O}(mk2^k)\)

一个细节是,为了方便状压,从 \(0\) 开始枚举 \(dp\) 数组的第一维。

由于时限较小,所以需要用一些方法优化常数。一种方法是使用 lowbit 加速转移。因为 \(s\) 实际上会有许多为 \(0\) 的位,使用 lowbit 就可以跳过这些位置。同时 \(dp\) 数组可以用滚动数组优化。

附上人傻常数大的代码:

#include<bits/stdc++.h>
using namespace std;
bool Mbegin;
void File_Work(){
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
}
const int M=3e3+8,ALL=1<<11,mod=1e9+7;
int lowbit(int x){
	return x&(-x);
}
int popcount(int x){
	int res=0;
	while(x){
		res+=x&1;
		x>>=1;
	}
	return res;
}
int n,m,k,all,dp[2][ALL+8];
void mian(){
	for(int s=0;s<=all+8;s++)
	    dp[0][s]=dp[1][s]=0;
	cin>>n>>m>>k;
	all=(1<<k)-1;
	for(int i=2;i<=n;i++){
		int father;
		cin>>father;
	}
	dp[1][0]=1;
	for(int u=0;u<m;u++){
		int cur=u&1;
		for(int s=0;s<=all+8;s++)
		    dp[cur][s]=0;
		for(int s=0;s<=all;s++){
			if(dp[cur^1][s]==0)
				continue;
			int t=s;
			while(t){
				int now=lowbit(t);
				int nxt=(s^now)<<1;
				if(nxt<=all)
					dp[cur][nxt]=(dp[cur][nxt]+dp[cur^1][s])%mod;
				t^=now;
			}
			int num=n+u+popcount(s);
			if((s<<1)<=all){
				dp[cur][s<<1]=(dp[cur][s<<1]+1ll*dp[cur^1][s]*(num*2-1)%mod)%mod;
				dp[cur][s<<1|1]=(dp[cur][s<<1|1]+1ll*dp[cur^1][s]*(num-1)%mod)%mod;
			}
		}
	}
	cout<<dp[(m-1)&1][0]<<'\n';
}
bool Mend;
int main(){
	File_Work();
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cerr<<fixed<<setprecision(3)<<(&Mbegin-&Mend)/1048576.0<<" MB\n\n\n";
	int testid,testnum;
	cin>>testid>>testnum;
	while(testnum--)
		mian();
	cerr<<"\n\n\n"<<fixed<<setprecision(0)<<1e3*clock()/CLOCKS_PER_SEC<<" ms";
	return 0;
}

注意到我们并没有记录原树的结构,因为 DP 转移时一定能满足第一个条件。