NOD2308B. 酒杯(glass)

liyixin0514 / 2024-11-17 / 原文

NOD2308B. 酒杯(glass)

题意

有一棵 \(n\) 层的满二叉树,有 \(m\) 次操作,每次操作从 \(2^n-1\) 个节点中随机选择一个节点染黑(可以重复染色),问使得每一层都至少有一个节点被染黑的方案数。\(n,m\le 2000\),答案对 \(10^9 + 7\) 取模。

solution

%%% 蔡队

显然问题可以简化成有 \(n\) 个点,第 \(i\) 个点有 \(2^{i-1}\) 种方式可以被染黑,问所有点都被染黑的方案数。

暴力就是枚举每次操作染了哪个点。

直接统计染上所有点的方案数是不好统计的。但是算至多可以染某些点的方案数是好算的。

考虑容斥。用点集 \(S\) 表示不允许染 \(S\) 里面的点。

子集枚举。(题解叫做子集反演

\[\begin{aligned} ans & =\sum_{S\subseteq [n]}(-1)^{|S|} (\sum_{i \not \in S} 2^i)^m\\ & = \sum_{S=0}^{2^n-1} (-1)^{count(S)} (2^n-1-S)^m \end{aligned} \]

根据直觉变一下形(实际上可以看做是设 \(S\) 表示允许染色的点集),那么有更加优美的式子:

\[f(n,m)=\sum_{S=0}^{2^n-1} (-1)^{n-count(S)} S^m \]

换个名字,写成:

\[f(n,m)=\sum_{i=0}^{2^n-1} (-1)^{n-count(i)} i^m \]

直接做这个东西的复杂度是 \(O(2^n\log m)\) 的。测试点 \(6\sim 8\) 必须(?)这个做法。

发现 \(f(n,m)\) 的个数是 \(nm\) 的,考虑变成递推转移。

假设我们考虑完前 \(n-1\) 个点,然后再加上第 \(n\) 个点,假设我们的 \(i\) 是把序号小的放在低位,有:

\[f(n,m)=\sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)} ((2i)^m - (2i+1)^m) \]

表示子集枚举前 \(i\) 个点的选择状态,乘方案数的时候枚举是否选择第 \(n\) 个数。\(2i\) 的状态表示不选,\(2i+1\) 的状态表示选择。

根据二项式定理\((x+y)^n = \sum_{i=0}^n \binom{n}{i} x^{n-i}y^i\)

\((2i+1)^m\) 拆开,变成 \(\sum_{k=0}^m \binom{m}{k} (2i)^k\)

有:

\[\begin{aligned} f(n,m) & =\sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)} (\sum_{k=0}^{m-1} - \binom{m}{k}(2i)^k )\\ & =\sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)-1} (\sum_{k=0}^{m-1} \binom{m}{k}(2i)^k )\\ & =\sum_{k=0}^{m-1} \binom{m}{k} 2^k \sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)-1} i^k\\ & =\sum_{k=0}^{m-1} \binom{m}{k} 2^k f(n-1,k) \end{aligned} \]

若 $i$ 是把序号大的放在低位,式子会变好看吗?

\[\begin{aligned} f(n,m) & =\sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)} ( i^m - (i+2^n)^m )\\ & = \sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)} (-\sum_{k=1}^m \binom{m}{k} i^{m-k} 2^{nk})\\ & = \sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)-1} (\sum_{k=1}^m \binom{m}{k} i^{m-k} 2^{nk})\\ & =\sum_{k=1}^m \binom{m}{k} 2^{nk} \sum_{i=0}^{2^{n-1}-1} (-1)^{n-count(i)-1} i^{m-k}\\ & = \sum_{k=1}^m \binom{m}{k} 2^{nk} f(n-1,m-k) \end{aligned} \]

本质上是一样的,而且并不会更加优美。

至此你获得了转移复杂度为 \(O(m)\) 的递推式。状态是 \(O(nm)\),总时间复杂度是 \(O(nm^2)\)

边界条件是 \(f(1,0)=0,f(1,k=1\sim m)=1\)

蔡队的分段打表方法(很有学习意义)

发现 \(f(n,m)\) 只和 \(f(n-1,k)\) 有关,因此对于第一维每隔 \(B\) 打一个长度为 \(m\) 的表。

然后发现空间不够。为什么 cplusoj 只能上传 50kb 的代码?解决方法是把表压成字符,使用表的时候进行解码操作。

题解提出倍增优化。

也许突破口是 \(f(n,m)\) 的具体意义感觉可以倍增?或许是因为 \(f(n,m)\) 只和 \(f(n-1)\) 有关,所以想要优化第一维的递推?类似于快速幂?

根据 \(f(2n,m)\) 的具体意义。考虑子集枚举前 \(n\) 个数的选择状态,乘方案数的时候枚举后 \(n\) 个数的选择状态。

\[\begin{aligned} f(2n,m) & =\sum_{i=0}^{2^{2n}-1} (-1)^{2n-count(i)} i^m\\ & = \sum_{i=0}^{2^n-1} (-1)^{n-count(i)} (\sum_{j=0}^{2^n-1} (-1)^{n-count(j)} (i+2^n j)^m)\\ & = \sum_{i=0}^{2^n-1} (-1)^{n-count(i)}(\sum_{j=0}^{2^n-1} (-1)^{n-count(j)} (\sum_{k=0}^m \binom{m}{k} i^{m-k} 2^{nk} j^k))\\ & = \sum_{k=0}^m \binom{m}{k} 2^{nk} \sum_{i=0}^{2^n-1} (-1)^{n-count(i)} i^{m-k} \sum_{j=0}^{2^n-1} (-1)^{n-count(j)} j^k \\ & = \sum_{k=0}^m \binom{m}{k} 2^{nk} f(n,m-k)f(n,k) \end{aligned} \]

好像……解决啦。Hooray!

但是 \(n\) 不一定是 \(2\) 的幂次怎么办呢?能否推广?

\[\begin{aligned} f(x+n,m) & =\sum_{i=0}^{2^x-1} (-1)^{x-count(i)} (\sum_{j=0}^{2^n-1} (-1)^{n-count(j)} (i+2^x j)^m)\\ & =\sum_{i=0}^{2^x-1} (-1)^{x-count(i)} (\sum_{j=0}^{2^n-1} (-1)^{n-count(j)} (\sum_{k=0}^{m} \binom{m}{k} i^{m-k} 2^{xk}j^k ))\\ & =\sum_{k=0}^{m} \binom{m}{k} 2^{xk} \sum_{i=0}^{2^x-1} (-1)^{x-count(i)} i^{m-k} \sum_{j=0}^{2^n-1} (-1)^{n-count(j)} j^k\\ & =\sum_{k=0}^{m} \binom{m}{k} 2^{xk} f(x,m-k) f(n,k) \end{aligned} \]

太优美了!可以推广!

更加优美的做法(来自 @gzyqwq 的建议)

其实不需要推广,因为我们已经有了 \(f(n) \gets f(n-1)\) 的公式,因此如果 \(n\) 是奇数就从 \(n-1\) 转移,否则从 \(\frac{n}{2}\) 转移。时间仍然是 \(\log\) 的。可以少推一个式子。

那么就做完了,对 \(n\) 这一维倍增,时间复杂度 \(O(m^2 \log n)\)

code

代码很好实现也很好理解。本题难度主要在推式子。

#include<bits/stdc++.h>
// #define LOCAL
#define sf scanf
#define pf printf
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
using namespace std;
typedef long long ll;
constexpr int mod=1e9+7;
constexpr int N=2e3+7;
int n,m;
ll ksm(ll a,ll b=mod-2) {
	ll s=1;
	while(b) {
		if(b&1) s=s*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return s;
}
ll add(ll a,ll b) { return a+b>=mod?a+b-mod:a+b; }
namespace sub1 {
	ll ans;
	void main() {
		rep(i,0,(1<<n)-1) ans=add(ans,add(mod,((n-__builtin_popcount(i))&1?-1ll:1ll)*ksm(i,m)));
		pf("%lld\n",ans);
	}
}
namespace sub2 {
	ll ans[N],tmp[N];
	ll f[30][N];
	ll mi2[N*N];
	ll binom[N][N];
	void main() {
		int lg=__lg(n);
		mi2[0]=1;
		rep(i,1,n*m) mi2[i]=add(mi2[i-1],mi2[i-1]);
		binom[0][0]=1;
		rep(i,1,m) {
			binom[i][0]=binom[i-1][0];
			rep(j,1,i) binom[i][j]=add(binom[i-1][j-1],binom[i-1][j]);
		}
		rep(i,1,m) f[0][i]=1;
		rep(i,1,lg) {
			rep(j,0,m) {
				rep(k,0,j) f[i][j]=add(f[i][j],binom[j][k]*mi2[k<<(i-1)]%mod*f[i-1][j-k]%mod*f[i-1][k]%mod);
			} 
		}
		int x=0;
		ans[0]=1;
		rep(i,0,lg) {
			if((n>>i)&1) {
				memcpy(tmp,ans,sizeof(tmp));
				rep(j,0,m) {
					ans[j]=0;
					rep(k,0,j) ans[j]=add(ans[j],binom[j][k]*mi2[x*k]%mod*tmp[j-k]%mod*f[i][k]%mod);
				}
				x+=1<<i;
			} 
		} 
		pf("%lld\n",ans[m]);
	}
}
int main() {
	#ifdef LOCAL
	freopen("my.out","w",stdout);
	#else
	freopen("glass.in","r",stdin);
	freopen("glass.out","w",stdout);
	#endif
	sf("%d%d",&n,&m);
	if(m==1145141919) sub1 :: main();
	else sub2 :: main();
}