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\) 次,的方案数。即
答案即为对所有度数集合求贡献之和,即
发现这一坨东西很难优化。看到 \(\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}\)。
设
枚举 \(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();
}
本文来自博客园,作者:liyixin,转载请注明原文链接:https://www.cnblogs.com/liyixin0514/p/18627846