重新学习组合计数

发现自己对关于多项式的一部分内容(比如下降幂多项式)一无所知,导致在计数题上经常失手,所以写了这么篇博客。
下降幂多项式:
定义:\(x^{\underline n}=\prod_0^{n-1}(x-i) = \frac{x!}{(x-n)!}\)
一个多项式可以和下降幂多项式唯一转换。
这在推公式时有一些用。
由于阶乘,下降幂多项式和egf,组合数有着巨大的联系。
它们通常能够互相转换。
所以在推式子时可能可以考虑这些转换。
\(x^{\underline n}\)的展开形式可以用倍增求。
下降幂多项式的点值:
构造关于下降幂点值的egf:\(\sum \frac{F(i)x^i}{i!}=\sum \frac{x^i}{i!}\sum _{j=0}^n\frac{i!}{(i-j)!}F_j\)
\(=\sum x^i\sum _{j=0}^n\frac{1}{(i-j)!}F_j\)
\(=\sum_{i=0}^n F_i\sum_{j=i}^{\inf} \frac{1}{(j-i)!}x^j=\sum_{i=0}^n F_i\sum_{j=0}^{\inf} \frac{1}{j!}x^j=\sum_{i=0}^n F_ix^ie^x=F(x)e^x\)
所以和\(e^x\)卷积就能得到点值的egf。
从点值得到egf:
\(EGF(F(x))=F(x)e^x\)
\(F(x)=EGF(F(x))e^{-x}\)
下降幂多项式乘法就是把点值求出来,然后对应位相乘,再换回下降幂。
注意点值某位\(x\)相乘后,要乘以\(x!\)
普通多项式转下降幂多项式:就是带入连续点值,然后使用上面的方法转成下降幂。
时间复杂度\(O(n\log_2^2n)\)
下降幂多项式转普通多项式:
求出多项式的egf,然后通过这个可以求出多项式的点值。
然后快速插值即可。
下降幂多项式乘法代码如下:

signed main(){
	jc[0]=ij[0]=1;
	for(int i=1;i<N;i++)
		jc[i]=jc[i-1]*i%mo;
	ij[N-1]=qp(jc[N-1],mo-2);
	for(int i=N-1;i;i--)
		ij[i-1]=ij[i]*i%mo;
	pl e,a,b,ii;
	int n,m,le;
	scanf("%lld%lld",&n,&m);
	le=n+m+5;
	e.resize(le);
	a.resize(n+1);
	b.resize(m+1);
	for(int i=0;i<=n+m;i++)
		e[i]=ij[i];
	for(int i=0;i<=n;i++)
		scanf("%lld",&a[i]);
	for(int i=0;i<=m;i++)
		scanf("%lld",&b[i]);
	a=a*e;
	b=b*e;
	ii=iv(e);
	for(int i=0;i<=le;i++)
		a[i]=a[i]*b[i]%mo*jc[i]%mo;
	a=a*ii;
	for(int i=0;i<=n+m;i++)
		printf("%lld ",a[i]);
}

组合计数中常见转化:
1.\(\sum i*a_i=\sum [a_i\geq j]\)
它的意义就是对于每个数\(x\),统计\(\geq x\)的方案数。
这样子可以把计数类问题转化为判定性问题。
2.拆幂贡献
把幂的每个元素拆进公式内。
3.只考虑式子的若干项
只考虑式子的若干项,可能更好做。
然后把原式向这个式子转化。
4.求导
很多题目都能求导。
5.利用等式的性质

小Q的序列:
地震后的幻想乡:
也是概率密度函数的学习笔记。
概率密度函数就是一个函数\(f(x)\)\(f(x)\)表示在\(x\)处取值的概率。
如果\(f(x)\)越大,在\(x\)处取值的概率就越大。
但是\(f(x)\)任意处的值为0。
使用如下公式描述函数在\([l,r]\)的取值可能性。
\(\int_l^rf(x)dx\)
概率密度函数必须满足:\(\int_{-\infty}^{+\infty}f(x)dx = 1\)
考虑一个sub-problem:求出原图上每条边为出现概率为\(x\),存在生成树的概率。
这是个经典dp问题。考虑正难则反,设\(f_s\)表示\(s\)集合内连通,\(g_s\)表示\(s\)集合不连通的概率。
根据最小数原理,每次枚举一个包含最小节点的集合\(t\)转移
\(f_s=1-f_tg_{s-t}*h(t,s-t)\)
其中\(h(t,s-t)\)表示\(t\)\(s-t\)集合内无边的概率。
显然\(h\)能够在\(O(3^n)\)的时间内预处理。
回到原题。
用归纳法可以证明,\(f_{all}\)是一个关于\(x\)的最多\(m\)次多项式。
答案要我们求出mst最大边权不超过\(x\)的概率。
\(g(x)\)表示mst最大边权不超过\(x\)的概率,则\(g'(x)\)表示mst最大边权的概率密度函数。
这是因为设\(h(x)\)表示mst最大边权的概率密度函数,则\(g=\int_0^ah(x)dx\)
显然\(g(0)=0\),这样子求导回来就是原函数。
于是把\(f\)设成一个多项式,然后使用\(f_s=1-f_tg_{s-t}*h(t,s-t)\)转移,最后求导即可。
\(\int_0^1xf_{all}'(x)\)就是答案。这是因为如果最大边的权值是\(x\),贡献就是\(x\)
所以和\(x\)相乘,把多项式在区间\([0,1]\)积分就是答案。
但是\(h(t,s-t)\)也要设成一个多项式。
实现上的细节:发现多项式的系数非常大。
使用如下公式进行转化:
\(\frac{a}{b}=\lfloor\frac{a}{b}\rfloor + \frac{a\mod b}{b}\)
这样子的好处:前面的\(\lfloor\frac{a}{b}\rfloor\)是整数,我们可以忽略整数部分,然后加上小数部分。
当b较小但是系数较大时能提升精度。
最后用如下语句更新答案即可:

while(ans<0)
		ans++;
	while(ans>=1)
		ans--;
#include<bits/stdc++.h>
using namespace std;
#define N 5010
#define int long long
int n,a[N],b[N],m;
struct pl{
	__int128 a[100];
	int l;
}f[N],pw[N],g,vt;
pl operator *(pl x,pl y){
	pl rd;
	memset(rd.a,0,sizeof(rd.a));
	rd.l=x.l+y.l-1;
	for(int i=0;i<x.l;i++)
		for(int j=0;j<y.l;j++)
			rd.a[i+j]+=x.a[i]*y.a[j];
	return rd;
}
pl operator +(pl x,pl y){
	pl rd;
	memset(rd.a,0,sizeof(rd.a));
	rd.l=max(x.l,y.l);
	for(int i=0;i<rd.l;i++)
		rd.a[i]=x.a[i]+y.a[i];
	return rd;
}
pl operator -(pl x,pl y){
	pl rd;
	memset(rd.a,0,sizeof(rd.a));
	rd.l=max(x.l,y.l);
	for(int i=0;i<rd.l;i++)
		rd.a[i]=x.a[i]-y.a[i];
	return rd;
}
pl qd(pl x){
	pl rd;
	memset(rd.a,0,sizeof(rd.a));
	rd.l=x.l-1;
	for(int i=0;i<rd.l;i++)
		rd.a[i]=x.a[i+1]*(i+1);
	return rd;
}
int lb(int x){
	return x&-x;
}
char st[N];
int tp;
void wr(__int128 x){
	if(x<0){
		putchar('-');
		x=-x;
	}
	tp=0;
	while(x){
		st[++tp]=x%10;
		x/=10;
	}
	for(int i=tp;i;i--)
		putchar(st[i]+'0');
	puts("");
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%lld%lld",&a[i],&b[i]);
		a[i]--;
		b[i]--;
	}
	g.l=2;
	g.a[0]=1;
	g.a[1]=-1;
	pw[0].l=1;
	pw[0].a[0]=1;
	for(int i=1;i<=m+2;i++)
		pw[i]=pw[i-1]*g;
	f[0].l=1;
	f[0].a[0]=1;
	for(int i=1;i<(1<<n);i++){
		int ll=lb(i);
		if(!(i-ll)){
			f[i].l=1;
			f[i].a[0]=1;
			continue;
		}
		for(int j=i;j;j=(j-1)&i)
			if(j&ll){
				int va=0,p=i-j;
				for(int k=1;k<=m;k++)
					if((i&(1<<a[k]))&&((i&(1<<b[k]))))
						va++;
				for(int k=1;k<=m;k++){
					if((p&(1<<a[k]))&&(p&(1<<b[k])))
						va--;
					else if((j&(1<<a[k]))&&(j&(1<<b[k])))
						va--;
				}
				f[i]=f[i]+f[j]*pw[va];
			}
		f[i].l=max(f[i].l,1ll);
		for(int j=0;j<f[i].l;j++)
			f[i].a[j]=-f[i].a[j];
		f[i].a[0]++;
	}
	long double ans=0;
	f[(1<<n)-1]=qd(f[(1<<n)-1]);
	vt.l=2;
	vt.a[1]=1;
	vt=vt*f[(1<<n)-1];
	for(int i=0;i<vt.l;i++)
		ans+=(vt.a[i]%(i+1))/((long double)i+1);
	while(ans<0)
		ans++;
	while(ans>=1)
		ans--;
	printf("%.6Lf",ans);
}

上个问题改成求出生成树边权和。
考虑每条边的贡献。
由期望的线性性,答案由每条边对答案的贡献构成。
\(f_i(x)\)表示mst上某边\(i\)在取值为\(x\)时在最小生成树的概率。
看上去一条边是否在mst上不好求,但是由于每条边的权值都是均匀实数,所以每条边的权值都是不同的。
考虑kruskal的过程,我们在之前插入了取值\(\leq x\)的所有边。
把取值\(\leq x\)的所有边插入图后判定边\(i\)两端点是否连通即可。
这样子就好做了。
可以用前文类似的dp计算答案,但是保证每个连通块不同时包含边\(i\)的两个端点。
时间复杂度\(O(3^mm^3)\)
jzoj5158

posted @ 2021-02-21 11:56  celerity1  阅读(85)  评论(0编辑  收藏  举报