组合数学练习

bzoj1042
用容斥。
由于硬币总数不多,枚举打破硬币个数限制的集合。
我们需要求出钦定一些硬币数(>a_i),其它任意的集合。
把总数(-=sum_{iin S}a_ic_i)后就变成了求恰好组成(v)的方案数。
用完全背包求出。

#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define int long long
int c[10],d[10],T,s,f[N];
signed main(){
	f[0]=1;
	for(int i=0;i<4;i++)
		scanf("%lld",&c[i]);
	scanf("%lld",&T);
	for(int i=0;i<4;i++)
		for(int j=0;j<N;j++)
			if(j>=c[i])
				f[j]+=f[j-c[i]];
	while(T--){
		for(int i=0;i<4;i++)
			scanf("%lld",&d[i]);
		scanf("%lld",&s);
		int ans=0;
		for(int i=0;i<16;i++){
			int v=s;
			for(int j=0;j<4;j++)
				if(i&(1<<j))
					v-=c[j]*(d[j]+1);
			int t=1;
			for(int j=0;j<4;j++)
				if(i&(1<<j))
					t=-t;
			if(v>=0)
				ans+=f[v]*t;
		}
		printf("%lld
",ans);
	}
}

[FJOI2017]矩阵填数
发现每个点的答案上界是它被覆盖到的矩形的值的最小值。
但是如果直接这样子算会算重。
由于(n)很小,考虑容斥,枚举(< v)的矩形的集合,然后运行上面的算法。
然而由于(H,W)很大,会TLE。
考虑离散化,把整个矩形按照限制的矩形的边界切开,如果存在(x=v)的边界,则把矩形在(x=v)处切开,(y)同理。
对于一个连通的区域,所有限制都是一样的,可以用快速幂计算。

ARC093D
感觉倒着填数很妙,虽然还是自己想出来了。
(1)的顺序对答案没有影响,可以钦定(1)在二叉树最左边,最后答案乘以(2^n)
考虑容斥,(1)到达根节点的条件:(1)到根所有节点的右节点的数(子树最小值)都不能打败(1)
枚举(1)到根的子树的右节点能够打败(1)的集合,设(f_s)表示钦定根的右节点能够打败(1)的集合,答案就是(sum_{s}(-1)^sf_s)
(f)可以dp求。
如果正着dp,则发现我们不能知道后面的状态,不可行。
考虑倒着状压dp,设(g_{i,s})表示倒着填到第(i)个,填了(s)集合内的数的方案。
注意到(s)事实上蕴含了后面填的空位个数这个条件。
转移:1.(i)不填,(g_{i,s}+=g_{i+1,s})
2.(i)填,枚举填的数(p),后面还没有填的数有(2^n-a_i-s)个,可以用组合数算出选出(2^k-1)个的方案数。
还要在子树上任意排列这些数,方案是((2^k)!)
转移到(g_{i,s+2^p})
然而事实上我们还是有一些数没有填。
发现未填的数有((i-s))个,这些数可以任意排列,答案乘以((i-s)!)
时间复杂度(O(nm2^n))

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 18
#define mo 1000000007
int n,m,a[N],f[N][(1<<N)],jc[1<<N],ij[1<<N],ans;
int qp(int x,int y){
	int r=1;
	for(;y;y>>=1,x=x*x%mo)
		if(y&1)
			r=r*x%mo;
	return r;
}
int c(int y,int x){
	if(y<0||x<0||y<x)
		return 0;
	return jc[y]*ij[x]%mo*ij[y-x]%mo;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=m;i++)
		scanf("%lld",&a[i]);
	f[m+1][0]=1;
	jc[0]=ij[0]=1;
	int sz=(1<<N)-1;
	for(int i=1;i<=sz;i++)
		jc[i]=jc[i-1]*i%mo;
	ij[sz]=qp(jc[sz],mo-2);
	for(int i=sz;i;i--)
		ij[i-1]=ij[i]*i%mo;
	for(int i=m;i;i--)
		for(int j=0;j<(1<<n);j++){
			f[i][j]=(f[i+1][j]+f[i][j])%mo;
			for(int k=0;k<n;k++)
				if(!(j&(1<<k))){
					f[i][j+(1<<k)]=(f[i][j+(1<<k)]+f[i+1][j]*c((1<<n)-a[i]-j,(1<<k)-1)%mo*jc[1<<k]%mo)%mo;
				}
		}
	for(int i=0;i<(1<<n);i++){
		int c=0,x=i;
		while(x){
			if(x&1)
				c++;
			x/=2;
		}
		if(c&1)
			ans=(ans-jc[(1<<n)-1-i]*f[1][i]%mo+mo)%mo;
		else
			ans=(ans+jc[(1<<n)-1-i]*f[1][i]%mo)%mo;
	}
	printf("%lld",ans*(1<<n)%mo);
}

arc096c
简单题
考虑容斥,枚举打破限制的集合,集合内的元素都只能选择0/1个,其它任意。
设打破了(i)个,(f(i))为打破(i)个答案,答案是(sum_{i=0}^n(-1)^if(i))
考虑分解成只能选择0/1个的集合的选择方案和其他集合的选择方案。
第一部分是(S(j,i))的和,可以递推
第二部分事实上可以考虑剩下的数组成的集合,有(2^{2^{n-i}})种,剩下的数也可以被插入第一部分的集合,如果选择了(s(j,i))计数,则方案是((2^{n-i})^j)

NOIP D
考试时没有打80分裸暴力,傻逼了
考虑经典的(>=)(=)转化,答案等于走到(i)还未结束的方案之和。
容易发现,只有某一维走过的(min,max)才有用。
对于每一维建立不等式数学模型,我们要求(a_i+maxleq w_i,a_i-mingeq 0)
移项后发现(a_i)事实上是在一个区间内的。
(k=1)答案就是区间长度。
否则使用乘法原理,把所有区间的长度乘起来就是答案。
暴力模拟发现只需要模拟到(max w_i)即可。
考虑每一维的不等式,发现它在走(n)步后就会纯循环,这事实上让我们可以得到更为优秀的做法。
暴力模拟走前(n)步的结果,考虑把所有(imod n)相同的(i)一起考虑。
假设它在走(n)步后,第(i)维的取值有(h_i)类。
(n)步后每走(n)步,(h_i)就会减少(e_i)
假设有(v)个循环,我们要求(sum_{i=1}^vsum_{j=1}^k(h_j-ie_k))
把后面的看成关于(i)的多项式(F(i)),则我们要求(sum_{i=1}^vF(i))
用暴力多项式乘法处理出(F)后,问题转化成自然数幂和,用插值解决。
时间复杂度(O(nk^2)),代码细节有点多
常数巨大,卡着过的。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mo 1000000007
#define N 500010
int n,k,f[20][20],g[N],h[N],jc[N],w[N],m[N],mn[N*2][11],mx[N*2][11],nw[N*2][11],e[N],ans,pl[13],sz,va[13],vv[N],c[N],d[N],ij[N];
int qp(int x,int y){
	int r=1;
	for(;y;y>>=1,x=x*x%mo)
		if(y&1)r=r*x%mo;
	return r;
}
int qz(int n,int k){
	g[0]=h[k+3]=1;
	for(int i=k+2;i;i--)
		h[i]=h[i+1]*(n-i)%mo;
	for(int i=1;i<=k+2;i++)
		g[i]=g[i-1]*(n-i)%mo;
	int r=0;
	for(int i=1;i<=k+2;i++){
		int va=f[k][i]*g[i-1]%mo*h[i+1]%mo*ij[i-1]%mo*ij[k+2-i]%mo;
		if((k-i+2)&1)r=(r-va+mo)%mo;
		else r=(r+va)%mo;
	}
	return r;
}
void ml(int *a,int *b){
	for(int i=0;i<=sz+1;i++)
		vv[i]=0;
	for(int i=0;i<=sz;i++)
		for(int j=0;j<2;j++)
			vv[i+j]=(vv[i+j]+a[i]*b[j]%mo)%mo;
	for(int i=0;i<=sz+1;i++)
		a[i]=vv[i];
	sz++;
}
signed main(){
	scanf("%lld%lld",&n,&k);
	jc[0]=ij[0]=1;
	for(int i=1;i<=k+4;i++){
		jc[i]=jc[i-1]*i%mo;
		ij[i]=qp(jc[i],mo-2);
	}
	for(int i=0;i<=k+2;i++)
		for(int j=1;j<=k+2;j++)
			f[i][j]=(f[i][j-1]+qp(j,i))%mo;
	for(int i=1;i<=k;i++)
		scanf("%lld",&w[i]);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&c[i],&d[i]);
	}
	int vt=1;
	for(int i=1;i<=k;i++)
		vt=vt*w[i]%mo;
	ans=(ans+vt)%mo;
	for(int i=1;i<=n*2;i++){
		for(int j=1;j<=k;j++){
			mn[i][j]=mn[i-1][j];
			mx[i][j]=mx[i-1][j];
			nw[i][j]=nw[i-1][j];
		}
		int p1=c[(i-1)%n+1];
		nw[i][p1]+=d[(i-1)%n+1];
		mx[i][p1]=max(mx[i][p1],nw[i][p1]);
		mn[i][p1]=min(mn[i][p1],nw[i][p1]);
		if(i<=n){
			vt=1;
			for(int j=1;j<=k;j++){
				if(w[j]>=(mx[i][j]-mn[i][j]))
					vt=vt*(w[j]-(mx[i][j]-mn[i][j]+mo)%mo+mo)%mo;
				else
					vt=0;
			}
			ans=(ans+vt)%mo;
		}
	}
	int ok=0;
	for(int i=1;i<=k;i++){
		e[i]=max(mx[2*n][i]-mx[n][i],mn[n][i]-mn[n*2][i]);
		if(e[i])
			ok=1;
	}
	if(!ok){
		puts("-1");
		return 0;
	}
	for(int i=1;i<=n;i++){
		memset(pl,0,sizeof(pl));
		sz=0;
		pl[0]=1;
		int vt=1,vs=0,sv=1e11;
		for(int j=1;j<=k;j++){
			if(e[j])
				sv=min(sv,(w[j]-(mx[n+i][j]-mn[n+i][j]))/e[j]);
		}
		for(int j=1;j<=k;j++){
			va[0]=(w[j]-(mx[n+i][j]-mn[n+i][j])+mo)%mo;
			if(w[j]<(mx[n+i][j]-mn[n+i][j]))
				vs=-1;
			va[1]=(-e[j]+mo)%mo;
			vt=vt*va[0]%mo;
			ml(pl,va);
		}
		if(vs!=-1){
			for(int j=0;j<=sz;j++)
				ans=(ans+qz(sv,j)*pl[j]%mo)%mo;
			ans=(ans+vt)%mo;
		}
	}
	printf("%lld",ans);
}

SubterraneanSun
简单题,考试时没有认真想。
如果我们得到了(A,B),则可以快速求出答案,不用题解所说的fft。
分类讨论去掉绝对值,(sum_{i=1}^ssum_{j=1}^ia_ib_j(i-j+1)^2=sum_{i=1}^ssum_{j=1}^ia_ib_j(i^2+j^2-2ij+1-j+i))
然后把式子拆出来,以(sum_{i=1}^ssum_{j=1}^ia_ib_ji^2)为例,我们事实上可以把(i)从和式中拿出来,这样子处理出(b_j)的前缀和就能计算。
(j>i)时同理。
时间复杂度(O(n)),比标算更为优秀。
再考虑如何求出(A,B),考虑求出(f_{i,j})
(s_i=sum_{j=1}^nf_{i,j}),那么(f_{i,j}=sum_{i=1}^n(f_{i-1,k}+j^i)=s_{i-1}+nj^i)
(s_i=sum_{j=1}^nf_{i,j}=sum_{j=1}^n(s_{i-1}+nj^i)=ns_{i-1}+nsum_{j=1}^nj^i)
如果我们得到了(s_{n-1}),那么可以在(O(m))的时间内计算出所有需要的(a,b)(前,后(300000)个)
(s)数组手动展开,(s_p=n^{p-1}(sum_{i=1}^nj)+sum_{i=1}^{p-1}(sum_{j=1}^nj^{p-i+1})n^i)
括号内的自然数幂和由于(m)不大,可以插值。
时间复杂度(O(m^2+n))

CF865G
花的OGF:(A(x)=(sum_{x^{p_i}})^N)
巧克力的生成函数:(B(x)=frac{1}{1-sum_{x^{c_i}}})
根据样例给的提示,我们要求花瓣有(i)个的方案数:(sum_i(A(x)[x^i])(B(x)[x^i]))
如果我们知道了(i),考虑(B)的实际意义,显然是背包dp。
(f_i)表示买了(i)块巧克力,显然有转移(f_i=sum_jf_{i-c_j})(f)的OGF就是(B)
(f)可以常系数线性齐次递推求 ,(B(x)[x^i]=x^imod P(x))(P)是特征多项式。
我们需要枚举(A(x))的每一项求出答案,这太慢了。
我们要求的:(A(x)[x^i])乘以(B(x)[x^i]),就是(i)(c)拼接成的方案数。
发现(mod P(x))事实上是线性的,也是可乘的。
根据常系数线性齐次递推的快速算法的意义,考虑手动展开(A),把(A(x)[x^v])展开成若干项(b)使得(b_i(B(x)[x^{max(c_i)-i}])=)答案。
这可以先求出所有(x^{p_i}mod P(x))的和,然后(N)次方。
求出(B(x))(max(c_i))可以背包。
感觉根据常系数线性齐次递推的快速算法的意义展开比较巧妙。

#include<bits/stdc++.h>
using namespace std;
#define N 250
#define mo 1000000007
#define int long long
int c[N*2+6],a[N],b[N],n,m,k,ans[N*2+6],va[N*2+6],vv[N*2+6];
void ml(int *a,int *d){
	for(int i=0;i<2*N;i++)
		c[i]=0;
	for(int i=0;i<N;i++)
		for(int j=0;j<N;j++)
			c[i+j]=(c[i+j]+a[i]*d[j])%mo;
	for(int i=2*N-1;i>=N;i--){
		for(int j=1;j<=m;j++){
			if(i>=b[j])
				c[i-b[j]]=(c[i-b[j]]+c[i])%mo;
		}
	}
	for(int i=0;i<N;i++)
		a[i]=c[i];
}
void qp(int *x,int y){
	for(int i=0;i<N*2;i++)
		ans[i]=0;
	ans[0]=1;
	for(;y;y>>=1,ml(x,x))
		if(y&1)
			ml(ans,x);
}
signed main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	for(int i=1;i<=m;i++)
		scanf("%lld",&b[i]);
	for(int i=1;i<=n;i++){
		for(int j=0;j<N*2;j++)
			va[j]=0;
		va[1]=1;
		qp(va,a[i]);
		for(int j=0;j<N;j++)
			vv[j]=(vv[j]+ans[j])%mo;
	}
	qp(vv,k);
	for(int j=0;j<N;j++)
		vv[j]=ans[j];
	for(int i=N-1;i;i--)
		for(int j=1;j<=m;j++)
			if(i>=b[j])
				vv[i-b[j]]=(vv[i-b[j]]+vv[i])%mo;
	printf("%lld
",vv[0]);
}

抗拒黄泉
(瞄了一眼题解)
考虑经典的等于和大于等于的转化,问题转化成统计时刻(i)还没有满足要求的期望。
(n+m)很小时考虑容斥,枚举没有选择的行/列,并且把它全部染成1。
这样子每一回合必须选择(0),设合法选择的概率为(p)
计算答案事实上是经典的期望公式,是(frac{1}{p})
(n+m)比较大时就不能这么做,但是(n*mleq 200)
考虑平衡规划,当(n<m)时,(nleq 14),那么可以容斥(n)
枚举哪些列没有被选择,并且把它全部染成1。
这样子每一回合必须选择(0),设合法选择的概率为(p)(p)只和现在(0)格子数量除以原来的(1)格子数量有关
那么就可以考虑dp了,设(f_{i,j,k})表示dp到前(i)行,选了(j)行,(0)格子有(k)个的方案数。
由于((-1)^j=(-1)^{j+2}),所以只需要记录(jmod 2)的结果。
转移显然,最后乘以(frac{1}{k})计算答案。

#include<bits/stdc++.h>
using namespace std;
#define N 210
int n,m,a[N][N],ct,f[30][2][N],b[N][N],cc,bz[N];
double ans;
int main(){
	//freopen("refuse.in","r",stdin);
	//freopen("refuse.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			scanf("%d",&a[i][j]);
			ct+=a[i][j];
		}
	if(n>m){
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				b[j][i]=a[i][j];
		for(int i=1;i<=m;i++)
			for(int j=1;j<=n;j++)
				a[i][j]=b[i][j];
		swap(n,m);
	}
	for(int i=0;i<(1<<n);i++){
		int bc=0;
		cc=0;
		memset(f,0,sizeof(f));
		memset(bz,0,sizeof(bz));
		for(int j=1;j<=n;j++)
			if(i&(1<<(j-1))){
				bz[j]=1;
				for(int k=1;k<=m;k++)
					cc+=a[j][k];
				bc^=1;
			}
		f[0][0][cc]=1;
		for(int j=0;j<m;j++){
			int cv=0;
			for(int k=1;k<=n;k++)
				if(!bz[k])
					cv+=a[k][j+1];	
			for(int k=0;k<2;k++)
				for(int l=cc;l<=ct-cv;l++){
					f[j+1][k][l]+=f[j][k][l];
					f[j+1][k^1][l+cv]+=f[j][k][l];
				}
		}
		for(int l=1;l<=ct;l++){
			double cf=((double)ct/(double)l);
			if(!bc){
				ans+=f[m][1][l]*cf;
				ans-=f[m][0][l]*cf;
			}
			else{
				ans+=f[m][0][l]*cf;
				ans-=f[m][1][l]*cf;
			}
		}			
	}
	printf("%.5lf
",ans);
}
原文地址:https://www.cnblogs.com/ctmlpfs/p/14653592.html