重新学习组合计数

发现自己对关于多项式的一部分内容(比如下降幂多项式)一无所知,导致在计数题上经常失手,所以写了这么篇博客。
下降幂多项式:
定义:(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}^nfrac{i!}{(i-j)!}F_j)
(=sum x^isum _{j=0}^nfrac{1}{(i-j)!}F_j)
(=sum_{i=0}^n F_isum_{j=i}^{inf} frac{1}{(j-i)!}x^j=sum_{i=0}^n F_isum_{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(nlog_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_igeq 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}=lfloorfrac{a}{b} floor + frac{amod b}{b})
这样子的好处:前面的(lfloorfrac{a}{b} floor)是整数,我们可以忽略整数部分,然后加上小数部分。
当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

原文地址:https://www.cnblogs.com/ctmlpfs/p/14425357.html