【luogu P5219】无聊的水题 I(prufer序列)(多项式)

无聊的水题 I

题目链接:luogu P5219

题目大意

给你 n 个点,然后问你有多少棵树满足它的最大度数的点是 m。

思路

看着这种构造树,我们有行列式的生成树计数,但这里感觉不是很好弄。
所以我们考虑用另一个东西:Prufer 序列。

Prufer 序列是每次选当前度数最小的点(随便一个),然后把它的父亲放进序列中,到剩下两个点的时候停止。
然后每个序列都对于一个不同的树,也就是说一共 \((n-2)!\) 棵树。

然后我们不难想到一个点的度数就是它在 Prufer 序列里面的出现次数加一。

那我们就可以把问题变成你有多少个长度为 \(n-2\) 的序列,然后你填 \(0\sim n\) 的数,每个数的最多次数不能超过 \(m-1\),而且至少有一个是 \(m-1\)
那后面这个至少我们可以容斥掉,就是用最多次数不能超过 \(m-1\) 的减去最多不能超过 \(m-2\) 的。

那问题就是你有多少个长度为 \(n-2\) 的序列,然后你填 \(0\sim n\) 的数,每个数的最多次数不能超过 \(m-1\)
考虑上 DP,设 \(f_{x,len}\) 为当前长度为 \(len\),已经用了 \(x\) 个数字有多少方案。

然后不难看出 \(O(n^3)\) 转移。
\(f_{x,len}=\sum\limits_{ i=0}^{min(len,m)}f_{x-1,len-i}*C(len,i)\)

然后分解 \(C\) 来化简:
\(f_{x,len}=\sum\limits_{ i=0}^{min(len,m)}f_{x-1,len-i}*\dfrac{len!}{i!(len-i)!}\)
\(\dfrac{f_{x,len}}{len!}=\sum\limits_{ i=0}^{min(len,m)}\dfrac{f_{x-1,len-i}}{i!(len-i)!}\)
\(\dfrac{f_{x,len}}{len!}=\sum\limits_{ i=0}^{min(len,m)}\dfrac{f_{x-1,len-i}}{(len-i)!}*\dfrac{1}{i!}\)

然后你会发现如果你设 \(F[x](len)=\dfrac{f_{x,len}}{len!}\)\(G(x)=\dfrac{1}{x!}\),那式子就变成了 \(F[x](len)=\sum\limits_{i=0}^{min(len,m)}F[x-1](len-i)G(i)\)

你发现每次都是一个卷积,而且每次乘的都是一样的。
那我们就可以直接用快速幂的形式,直接 \(O(n\log^2n)\),不过最后记得乘上 \(len!\) 才是答案。
好像也可以用那个什么指数多项式做到更优的 \(O(n\log n)\),但是我没学awa。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
#define mo 998244353
#define clr(f, x) memset(f, 0, sizeof(ll) * (x))
#define cpy(f, g, x) memcpy(f, g, sizeof(ll) * (x))

using namespace std;

const ll N = 50001;
ll n, m, jc[N << 2], inv[N << 2], x[N << 2];
ll GG[N << 2], an[N << 2], l_size, limit, rek[N << 2];
ll G = 3, Gv;

ll ksm(ll x, ll y) {
	ll re = 1;
	while (y) {
		if (y & 1) re = re * x % mo;
		x = x * x % mo;
		y >>= 1;
	}
	return re;
}

ll inv_(ll x) {
	return ksm(x, mo - 2);
}

void NTT(ll *f, ll limit, ll op) {
	for (int i = 0; i < limit; i++)
		if (i < an[i]) swap(f[i], f[an[i]]);
	
	for (int mid = 1; mid < limit; mid <<= 1) {
		ll Wn = ksm(op == 1 ? G : Gv, (mo - 1) / (mid << 1));
		for (int R = (mid << 1), j = 0; j < limit; j += R) {
			ll w = 1;
			for (int k = 0; k < mid; k++, w = w * Wn % mo) {
				ll x = f[j + k], y = w * f[j + mid + k] % mo;
				f[j + k] = (x + y) % mo;
				f[j + mid + k] = (x - y + mo) % mo;
			}
		}
	}
	
	if (op == -1) {
		ll invl = inv_(limit);
		for (int i = 0; i < limit; i++)
			f[i] = f[i] * invl % mo;
	}
}

void cheng(ll *f, ll *g) {
	for (int i = 0; i < limit; i++)
		f[i] = f[i] * g[i] % mo;
}

ll xl_ksm(ll *X, ll p, ll n, ll pl) {//直接就快速幂,把乘变成多项式的乘即可
	cpy(x, X, p); clr(rek, p); rek[0] = 1;
	limit = 1; l_size = 0;
	while (limit <= p * 2) {
		limit <<= 1; l_size++;
	}
	for (int i = 0; i < limit; i++) {
		an[i] = (an[i >> 1] >> 1) | ((i & 1) << (l_size - 1));
	}
	
	while (n) {
		if (n & 1) {
			NTT(x, limit, 1); NTT(rek, limit, 1);
			cheng(rek, x); NTT(x, limit, -1); NTT(rek, limit, -1);
			clr(rek + p + 1, limit - p);
		}
		NTT(x, limit, 1);
		cheng(x, x); NTT(x, limit, -1);
		clr(x + p + 1, limit - p);
		
		n >>= 1;
	}
	return rek[pl];
}

ll work(ll len, ll n, ll m) {
	clr(GG, N << 2);
	for (int i = 0; i <= m; i++) GG[i] = inv[i];
	
	return xl_ksm(GG, len + 1, n, len) * jc[len] % mo;
}

int main() {
	scanf("%lld %lld", &n, &m);
	
	Gv = inv_(G);
	jc[0] = 1; for (int i = 1; i <= N << 2; i++) jc[i] = jc[i - 1] * i % mo;
	inv[N << 2] = inv_(jc[N << 2]);
	for (int i = (N << 2) - 1; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mo;
	
	printf("%lld", (work(n - 2, n, m - 1) - work(n - 2, n, m - 2) + mo) % mo);
	
	return 0;
}
posted @ 2022-01-22 15:41  あおいSakura  阅读(45)  评论(0编辑  收藏  举报