zoukankan      html  css  js  c++  java
  • 重新学习组合计数

    发现自己对关于多项式的一部分内容(比如下降幂多项式)一无所知,导致在计数题上经常失手,所以写了这么篇博客。
    下降幂多项式:
    定义:(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

  • 相关阅读:
    3.18 每日一练
    第二章 练习
    第一章 练习
    Redis常用操作大全和Python操作Redis
    vue学习【第七篇】:Vue之导入Bootstrap
    Vue学习【第六篇】:Vue-cli脚手架(框架)与实战案例
    Redis 安装,配置以及数据操作
    vue学习【第五篇】:Vue组件
    vue学习【第三篇】:vue之node.js的简单介绍
    Vue学习【第二篇】:ES6简单介绍
  • 原文地址:https://www.cnblogs.com/ctmlpfs/p/14425357.html
Copyright © 2011-2022 走看看