escape

escape

题意

\(n\) 个点,\(k\) 个连通块,每个连通块有 \(s_i\) 个点,每个连通块内部是完全图。你需要添加 \(k-1\) 条边使整个图连通。设每个连通块度数是 \(d_i\),一个加边方案的贡献就是 \(\prod_{i=1}^k d_i!\)。问所有加边方案的总贡献。

\(k \le 7000,n \le 10^9\)

思路

生成函数做法

来自 https://www.luogu.com/paste/rldowm8l

由于国际站太难上,就复制过来了。

\(\text{prufer}\) 序列得方案数为 \(\sum\limits_{\sum(d_i-1)=k-2}\binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}\prod s_i^{d_i}\)

所以答案即 \(\sum\limits_{\sum(d_i-1)=k-2}\binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}\prod s_i^{d_i}d_i!=(k-2)!\sum\limits_{\sum(d_i-1)=k-2}\prod s_i^{d_i}d_i\)

可以暴力背包做到 \(O(k^3)\),但显然不够

这种背包型的计数基本都可以用 \(\text{GF}\) 来表示,答案可以表示成 \((k-2)![x^{k-2}]\prod\limits_{i=1}^k\sum\limits_{j=0}^\infty s_i^{j+1}(j+1)x^j\)

这个形式很眼熟,因为有 \((x^n)'=nx^{n-1}\),所以后面那个和式可以表示成 \((\sum\limits_{j=0}^\infty s_i^jx^j)'=(\frac1{1-s_ix})'=\frac{s_i}{(1-s_ix)^2}\)

也即我们要求 \(\prod\limits_{i=1}^k(1-s_ix)\),可以做到 \(O(k\log^2k)\),应该是没法单log吧……?

不管是从常数来看,还是实现方便来看,\(O(k^2)\) 做暴力卷积和求逆显然比fft更优()

#include <iostream>
#include <cstdio>
#define mod 998244353
using namespace std;
typedef long long ll;
ll n,k,a[7001],f[7001],g[7001];
int main() {
	scanf("%lld%lld",&n,&k);
	n=k;
	k-=2;
	for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
	f[0]=1;
	for(int i=1;i<=n;++i)
		for(int j=k;j>=1;--j) f[j]=(f[j]-f[j-1]*a[i])%mod;
	for(int i=0;i<=k;++i) {
		g[i]=f[i];
		f[i]=0;
	}
	for(int i=0;i<=k;++i)
		for(int j=k-i;j>=0;--j) f[i+j]=(f[i+j]+g[i]*g[j])%mod;
	for(int i=0;i<=k;++i) {
		g[i]=f[i];
		f[i]=0;
	}
	f[0]=1;
	for(int i=1;i<=k;++i) {
		ll sum=0;
		for(int j=1;j<=i;++j) sum=(sum+g[j]*f[i-j])%mod;
		f[i]=-sum;
	}
	ll ans=f[k];
	for(int i=1;i<=n;++i) ans=ans*a[i]%mod;
	for(int i=1;i<=k;++i) ans=ans*i%mod;
	printf("%lld",(ans+mod)%mod);
	return 0;
}

变成 \(k\) 个点,每个点有点权 \(s_i\)。以一个连通块为端点,方案数需要乘上 \(s_i\)

相当于是问有标号有点权无根树,每种建树方式还要计算贡献,求所有建树方案贡献之和。

转换成 prufer 序列做。没学过可以去看 OI Wiki,写得很好。

把树映射到一个值域在 \([1,k]\) 的,长度为 \(k-2\) 的序列上面。每个点的度数 \(d_i\) 等于这个编号在 prufer 序列中的出现次数 \(+1\)

根据凯莱公式,\(k\) 个点生成有标号无根树的方案数是 \(k^{k-2}\)

考虑对一棵已知的树计算贡献,贡献是其度数连乘,以及每个点每多一个度数,就会贡献乘上它的点权,意义为可以选择连通块内任意一个节点作为端点,即 \(\prod_{i=1}^k s_i^{d_i} \prod_{i=1}^k d_i!\)

发现只和每个点的度数有关,和树的形态什么的无关。对于确定的度数序列 \(\{ d_i \},\sum d_i = 2k-2\)。生成这个度数序列的方案数就是,在长度为 \(k-2\) 的 prufer 序列上,把数字 \(i,i\in [1,k]\) 填写 \(d_i-1\) 次,的方案数。即

\[\begin{aligned} & \binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1}\\ = & \frac{(k-2)!}{(d_1-1)!(k-2-(d_1-1))!}\times \frac{(k-2-(d_1-1))!}{(d_2-1)!(k-2-(d_1-1)-(d_2-1))!}\times \cdots\\ = & \frac{(k-2)!}{\prod_{i=1}^k (d_i-1)!} \end{aligned} \]

答案即为对所有度数集合求贡献之和,即

\[\begin{aligned} & \sum_{d_i\ge 1,\sum d_i = 2k-2} \binom{k-2}{d_1-1,d_2-1,\cdots,d_k-1} \prod_{i=1}^k s_i^{d_i} \prod_{i=1}^k d_i\\ = & \sum_{d_i\ge 1,\sum d_i = 2k-2} \frac{(k-2)!}{\prod_{i=1}^k (d_i-1)!} \prod_{i=1}^k s_i^{d_i} \prod_{i=1}^k d_i!\\ = & \sum_{d_i\ge 1,\sum d_i = 2k-2} (k-2)! \prod_{i=1}^k s_i^{d_i} \prod_{i=1}^k d_i\\ = & (k-2)! \sum_{d_i\ge 1,\sum d_i = 2k-2} \prod_{i=1}^k s_i^{d_i} \prod_{i=1}^k d_i\\ \end{aligned} \]

发现这一坨东西很难优化。看到 \(\sum d_i = 2k-2\),发现我不会多项式做法,而且后面一坨连乘也不好处理吧,发现 \(k \le 7000\),考虑 \(O(k^2)\) DP。

问题变成,你有 \(2k-2\) 个物品,你要分成 \(k\) 段,每段不能为空,一个长度为 \(len\) 的段,而且是第 \(i\) 段对答案的贡献是乘上 \(len \times s_i^{len}\)

\(f_{i,j}\) 表示处理到第 \(i\) 段,第 \(i\) 段以 \(j\) 结尾,所有方案的贡献只和。发现我们转移只关心第几段以及上一段末尾在哪里,所以这个是好转移的。

暴力枚举上一段末尾 \(q\),转移复杂度 \(O(k^3)\)

考虑到当前第 \(i\) 段长度从 \(len\) 变成 \(len+1\) 的时候,贡献从 \(len \times s_i^{len}\) 变成 \((len+1) \times s_i^{len+1}\)。对于所有的 \(q\),当 \(j\) 变成 \(j+1\) 的时候,转移系数全部先乘上一个 \(s_i\),对每一个 \(q\) 的转移系数加上 \(s_{len} \times f_{i-1,q}\)

\[sum_1=\sum_{q=1}^{j-1} s_{j-q} \times f_{i-1,q}\\ sum_2=\sum_{q=1}^{j-1} (j-q)\times s_i^{j-q} f_{i-1,q}\]

枚举 \(i\),枚举 \(j\),每次 \(j \gets j+1\) 时更新 \(f_{i,j}\gets sum_2\),然后更新 \(sum_1,sum_2\)。复杂度 \(O(k^2)\)

code

我不知道为什么 std 写了 5kb。好写的。

#include<bits/stdc++.h>
#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;
namespace ocean {
	constexpr int N=7e3+7,mod=998244353;
	int add(int a,int b) { return a+b>=mod ? a+b-mod : a+b; }
	void _add(int &a,int b) { a=add(a,b); }
	int mul(int a,int b) { return 1ll*a*b%mod; }
	void _mul(int &a,int b) { a=mul(a,b); }
	int n,k;
	int s[N];
	int ans;
	int ksm(int a,int b=mod-2) {
		int s=1;
		while(b) {
			if(b&1) _mul(s,a);
			_mul(a,a);
			b>>=1;
		}
		return s;
	}
	int jc[N];
	int mi[N][N];
	void init() {
		jc[0]=1;
		rep(i,1,k-1) jc[i]=mul(jc[i-1],i);
		rep(i,1,k) {
			mi[i][0]=1;
			rep(j,1,k-1) mi[i][j]=mul(mi[i][j-1],s[i]);
		}
	}
	int f[N][N];
	int sum,sum2;
	void main() {
		sf("%d%d",&n,&k);
		rep(i,1,k) sf("%d",&s[i]);
		init();
		f[0][0]=1;
		rep(i,1,k) {
			sum=sum2=mul(f[i-1][i-1],s[i]);
			rep(j,i,k*2-2-(k-i)) {
				f[i][j]=sum2;
				_add(sum,f[i-1][j]);
				_mul(sum,s[i]), _mul(sum2,s[i]);
				_add(sum2,sum);
			}
		}
		pf("%d\n",mul(f[k][k*2-2],jc[k-2]));
	}
}
int main() {
	#ifdef LOCAL
	freopen("my.out","w",stdout);
	#endif
	ocean :: main();
}
posted @ 2024-12-24 15:42  liyixin  阅读(14)  评论(0编辑  收藏  举报