【集训】组合计数

题单
不是自己写的,总结的

交换求和顺序:

  • i=1nσ0(i)=i=1ndi1=d=1ni=1n[di]=d=1nnd
  • 给定序列 a ,求其所有区间的区间和:答案是 i=1nai×i×(ni+1)

二项式定理:

(a+b)n=k=0n(nk)ankbk

常见组合恒等式:

(nm)(mk)=(nk)(nkmk)(nmk)

先从 n 个中选出 m 个,再从选出的 m 个里面选出 k 先从 n 个中选出最终的 k 个,再从剩下的 nk 个中选出第一步选中但第二步末选中的 mk 个。

i(ni)=n(n1i1)

利用(nm)=n!m!(nm)! 展开即可得到证明。


j=wk(xj)=i=1x(i1w1)(nikw)

相当于从 n 个元素中选出 k 个,但前 x 个元素中必须选出至少 w 个;这等价于选出的第 w 个元素的位置在 x 之前。


i=0m(n+ii)=(n+m+1n+1)

考察 n+m+1 元集的 n+1 元子集的最后一个元素。


i=0n(ni)(mki)=(n+mk)

n+m 个元素中选出前 k 个,考察前 n 个元素中选出的数的个数。

常见模型:

n 种物品,第 i 种物品有 ai 个,认为相同物品之间互不区分,则将这 ai 个物品排列的方案数为

(i=1nai)!ai!

n 个相同的物品无序地分为 m 组,每组都非空的方案数:相当于往 n1 个空位里面插入 m1 个板,因此方案数为 (n1m1)

如果不要求每组非空,考虑直接给每组都加一个,方案数为 (n+m1m1)


求方程 i=1mxi=n,xiZ,xi0 的整数解个数:(n+m1m1)

如果再给出序列 y1...m,要求 xiyi
相当于 i=1m(xiyi)=ni=1myi,故方案数为 (n+mi=1myi1m1)

如果要求 xiyi 呢?此时有两种做法:

  • DP:f(i,j) 表示前 i 个数和为 j 的方案数,转移用前缀和优化。O(nm)
  • 容斥:钦定 S{1,2,,m} 以内的限制不被满足,其余的不管,转化为要求 xi>yi 的情形。O(2m×poly(m))

求有多少长为 2n 的合法括号序列。答案记作 Cn,这就是卡特兰数。

把左括号看成往右走,有括号看成往上走,那么这个就对应一条 (0,0)(n,n) 的路径,且不能穿过 y=x(但可以触碰)。

路径一共有 (2nn) 种,考虑怎么算穿过的方案数,也就是触碰了 y=x+1 这条直线。

我们在最后触碰的位置 (p,p+1)(0,0)(p,p+1) 这一部分翻折,发现就唯一对应一条 (1,1)(n,n) 的路径,于是方案数就是 (2nn1)。于是我们有以下这样的关系

Cn=(2nn)(2nn1)

这就是卡特兰数。

另一方面,从括号序列的角度,我们还能给出另一种递推式:枚举第一个左括号匹配的右括号的位置为 2k,有

Cn=k=1nCk1Cnk

其中 C0=1

卡特兰数 Cn 常用套路

  • 对角线不相交的情况下,将一个 n 条边的凸多边形区域分成三角形区域的方法数,即三角剖分的方案数。
  • 一个大小为 n 的栈,依次入栈 1,2,,n,但可以在任意时机出栈,问有多少种出栈序。
  • n 个节点的无标号有根二叉树的个数。

P2671 [NOIP2015 普及组] 求和

现在考虑一组中有 m 个格子,则这一组对答案的贡献为

i=1mj=1i1(i+j)×(ai+aj)

若枚举 i,j 暴力计算,则复杂度为 O(n2),会 TLE,所以需要进一步优化

现在我们把这个式子拆开(把括号打开)

(1)i=1mj=1i1(i+j)×(ai+aj)=i=1mj=1i1(i×ai+j×ai+i×aj+j×aj)=i=1mj=1i1i×ai+i=1mj=1i1j×ai+i=1mj=1i1i×aj+i=1mj=1i1j×aj

第一个式子可以化简为

i=1mi×ai×(i1)

第二个式子可以化简为

i=1mai×(i1)×i2

第三个式子 i=1mj=1i1i×aj 比较麻烦, 我们可以考虑每个 aj 对答案的贡献

比如考虑 j=1 时的 a1,我们知道它的贡献应该是 i=2mi×a1

也就是说,当 i=2m 时,会存在 i×a1 这一项

考虑 j=2 时的 a2 ,我们知道它的贡献应该是 i=3mi×a2

也就是说,当 i=3m 时,会存在 i×a2 这一项

以此类推,第三个式子可以化简为

j=1maj×i=j+1mi=j=1maj×(j+1+m)×(mj)2

对于第四个式子,我们可以考虑每个 j×aj 对答案的贡献

i=2m 时,1×a1 这一项会出现一次

i=3m 时,2×a2 这一项会出现一次

以此类推,第四个式子可以化简为

i=1mj=1i1j×aj=j=1mj×aj×i=j+1m1=j=1mj×aj×(mj)

整理一下,可得:

(2)i=1mj=1i1(i+j)×(ai+aj)=i=1m(i×ai×(i1)+ai×(i1)×i2+ai×(i+1+m)×(mi)2+i×ai×(mi))=i=1mai×(i2i+i2i2+m×i+m+m2i2im×i2+m×ii2)=i=1mai×(i×m2i+m2+m2)=i=1mi×ai×(m2)+i=1mai×m2+m2

简单维护即可。

#include <bits/stdc++.h>
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
#define FOR(i,a,b) for(int i=(a);i<=(b);++i)
#define ROF(i,a,b) for(int i=(a);i>=(b);--i)
#define U unsigned
#define LL long long
using namespace std;
template<class T>
inline void read(T &a){ register U LL x=0,t=1; register char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') t=-1; ch=getchar();} while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } a=x*t;}
inline void print(LL x){if(x<0)putchar('-'),x=-x;if(x>9) print(x/10);putchar(x%10+'0');}

const int mod=10007;
const int maxn=1e5+5;
int n,m;
int a[maxn],b[maxn];
int s1[maxn][2],s2[maxn][2];
int ans;
void sovle(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++) {
		cin>>b[i];
		s1[b[i]][i%2]++;
		s2[b[i]][i%2]=(s2[b[i]][i%2]+a[i])%mod; 
	}
	for(int i=1;i<=n;i++){
		int y=b[i];
		ans+=i*(s2[y][i%2]+a[i]*(s1[y][i%2]-2)%mod)%mod;
		ans%=mod;
	}
	cout<<ans<<endl;
	
}

signed main(){
    sovle();
 	return 0;
}

把同色,同奇偶性的放在一起考虑。设当前分离出来的这一组编号分别为 x1,,xk,权值分别为 y1,,yk,则贡献为

(3)i=1kj=i+1k(xi+xj)(yi+yj)=i=1kj=i+1kxiyi+xiyj+xjyi+xjyj=i=1k(ki)xiyi+xij=i+1kyj+yik=i+1nxj+k=i+1nxjyj

预处理 x,y,xy 的后缀和即可。时间复杂度为 O(n)

AT_abc266_g [ABC266G] Yet Another RGB Sequence

首先我们考虑把 rg 算成字符 \#

题面即变为:求 RkrGkgBbk\# 组成不含 rg 的序列的方案数。

此时,我们先把 g,b\# 排好,方案即为 CGk+B+kGk×CB+kk

再考虑把 Rkr 加入,此时不能将 r 放在 g 前面,所以这一步有 CRk+B+kRk 种方案。

故共有 CG+BGk×CB+kk×CR+BRk 种方案。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int mod=998244353;
ll a,b,c,d,ans; 
ll poww(ll a,ll b){
	ll res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
ll C(int n,int m){
	if(m==0) return 1;
	ll res=1;
	for(int i=n;i>=n-m+1;i--)
		res=res*i%mod;
	for(int j=1;j<=m;j++)
		res=res*poww(j,mod-2)%mod;
	return res;
}
int main(){
	cin>>a>>b>>c>>d;
	a-=d,b-=d;
	ans=C(c+d+b,b)*C(d+c,d)%mod*C(c+d+a,a)%mod;
	cout<<ans<<endl;
	return 0;
} 

AT_abc240_g [ABC240G] Teleporting Takahashi

N<|X|+|Y|+|Z| 时无解。

否则,考虑先算出将 a 步分配到一个长为 b 的维度上,发现方案数是

(aa+b2)

不妨考虑两维的情况

F(N)=x=0N(Nx)(xx+X2)(NxN+Yx2)=x=0NN!(x+X2)!(xX2)!(N+Yx2)!(NYx2)!=N!(NXY2)!(N+X+Y2)!x=0N(NXY2)!(N+X+Y2)!(x+X2)!(xX2)!(N+Yx2)!(NYx2)!=N!(NXY2)!(N+X+Y2)!x=0N(NXY2xX2)(N+X+Y2N+Yx2)=(NN+X+Y2)(NN+XY2)

扩展到三维,答案就是

z=0NF(Nz)(Nz)(zz+Z2)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=998244353,N=2e7+5;
ll fac[N],ifac[N],n,X,Y,Z;
int ksm(int x,int y,int p=mod){
	int ans=1;
	for(int i=y;i;i>>=1,x=1ll*x*x%p)if(i&1)ans=1ll*ans*x%p;
	return ans%p;
}
ll inv(int x,int p=mod){
	return ksm(x,p-2,p)%p;
}
void add(ll &x,ll v){
	x+=v;
	if(x>=mod) x-=mod;
}

ll C(int x,int y){
	if(x<y||y<0) return 0;
	return 1ll*fac[x]*ifac[y]%mod*ifac[x-y]%mod;
}
ll F(int m){
	if((m+X+Y)%2!=0) return 0;
	return 1ll*C(m,(m+X+Y)/2)*C(m,(m+X-Y)/2)%mod;
}

signed main(){
	cin>>n>>X>>Y>>Z; 
	fac[0]=1;
	for(int i=1;i<=n*2;i++) fac[i]=1ll*fac[i-1]*i%mod;
	ifac[n*2]=inv(fac[n*2]);
	for(int i=n*2-1;i>=0;i--) ifac[i]=1ll*ifac[i+1]*(i+1)%mod;
	ll ans=0;
	for(int z=0;z<=n;z++) 
		if((z+Z)%2==0) 
			add(ans,1ll*F(n-z)*C(n,z)%mod*C(z,(z+Z)/2)%mod);
			
	cout<<ans<<endl;
	return 0;
}

容斥原理 & 二项式反演

如果有 n 个约束 p1,p2,,pn,我们要求计数的对象符合所有约束;但符合约束时不易计算,反而是不符合约束时容易计算,此时便可以采用容斥的手法。

具体地,我们枚举一个约束的集合 S,钦定 S 中的约束必须不被满足,S 之外的约束可以满足也可以不满足,再附上 (1)|S| 的容斥系数,即可算出符合所有约束的方案数。

例:计算有多少个长为 n 的排列 a 满足:对每个 i=1,2,,n,都有 aii

如果各个约束之间本质相同,即钦定违反的集合 S 之后,方案数只和 S 的大小 |S| 有关,我们就得到了二项式反演

gi=j=in(ji)fjfi=j=in(1)ji(ji)gj

gi 的实际含义是,钦定某 i 个元素被选中,其余不做要求的方案数。也就是说,选中恰好 j 个元素的方案会被算 (ji) 次,即 gi=j=in(ji)fj


常见容斥模型:

  • n 个球排成一列,每个球可以染成 m 种颜色中的一种,相邻球的颜色不能相同,但是每种颜色至少出现一次,问方案数。

  • 计算有多少个长为 m 的序列 x 满足:i=1mxi=n,且对每个 i=1,2,,m0xiyi,其中 yi 是给定的序列。

P10596 BZOJ2839 集合计数

给定 n,k。令 S0={1,2,3,,n},S1={SSS0},求有多少 S1 的子集 S 满足 |SS1S|=k

仍然是钦定。设 f(k) 表示「至少」k 个元素是集合的交集,即钦定 k 个元素作为集合的交集,剩余不做限制的方案数,会有重复。有:

f(k)=(nk)(22nk1)

g(k) 表示恰好 k 个元素是集合的交集的方案数,有:

f(k)=i=kn(ik)g(i)

二项式反演得:

g(k)=i=kn(1)ik(ik)f(i)=i=kn(1)ik(ik)(ni)(22ni1)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 7,mod = 1000000007;
int ksm(int a, int b, int md) {
    int res = 1;
    while(b) {
        if(b & 1) res = res * a % md;
        a = a * a % md;
        b >>= 1;
    }
    return res % md;
}
int fac[N], infac[N];
int getc(int a, int b) {
    return fac[a] % mod * infac[b] % mod * infac[a - b] % mod;
}
signed main() {
    int n, k;
    cin >> n >> k;
    fac[0] = infac[0] = 1;
    for(int i = 1; i < N; i ++) {
        fac[i] = fac[i - 1] * i % mod;
        infac[i] = ksm(fac[i], mod - 2, mod) % mod;
    }   
    int ans = 0,p = getc(n, k);
    n -= k;
    for(int i = n; i >= 0; i --) {
        if(i % 2 == 0)
            ans = (ans + getc(n, i) % mod * (ksm(2ll, ksm(2ll, n - i, mod - 1), mod) - 1) % mod) % mod;
        else 
            ans = (ans - getc(n, i) % mod * (ksm(2ll, ksm(2ll, n - i, mod - 1), mod) - 1) % mod + mod) % mod;
        ans %= mod;
    }
    cout << (ans * p + mod) % mod << endl;
}

P4859 已经没有什么好害怕的了

给出 n 个数 ai ,以及 n 个数 bi ,要求两两配对使得 a>b 的对数减去 a<b 的对数等于k

我们假设 a>b 对数为 x ,可以求得 x=n+k2 。 我们令 fi,j 表示前 ia 中,选了 j 组满足 a>b 的方案数。

容易得到 dp 方程 fi,j=fi1,j+(lij+1)×fi1,j1 其中 li 表示 b<ai 的个数。

我们记 gi=fn,i×(ni)! 即满足 a>b 的组数 i 的方案数,再令 fi 表示恰好满足 a>b 的组数 =i 的方案数。

容易发现对于 i>j fi 恰好在 gj 中算了 (ij) 次。

那么存在 g(k)=i=kn(ik)f(i) 由二项式反演得 f(k)=i=kn(1)ik(ik)g(i) 直接求解即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 2000+5, P = 1e9+9;
long long n, k, a[N], b[N], l[N], f[N][N], fac[N], ifac[N], g[N];
long long C(int n, int m) {
	return 1ll*fac[n]*ifac[m]%P*ifac[n-m]%P; 
}
long long power(long long x,long long y){ 
	long long res = 1;
	while(y){
		if(y & 1)
		    res = res * x % P;
		x = x * x % P;
		y >>= 1;
	}
	return res;
}
int main(){
	cin>>n>>k;
	if ((n+k)&1) return puts("0"),0;   
	k = (n+k)/2;
	ifac[0] = ifac[1] = fac[0] = fac[1] = 1;
	for(int i = 1;i <= n;i++)
	    fac[i] = fac[i - 1] * i % P; 
	ifac[n] = power(fac[n],P - 2);
	for(int i = n;i >= 1;i--)
	    ifac[i - 1] = ifac[i] * i % P;
	for (int i = 1; i <= n; i++) cin>>a[i];
	for (int i = 1; i <= n; i++) cin>>b[i];
	sort(a+1, a+n+1); 
	sort(b+1, b+n+1);
	int ll = 0;
	for (int i = 1; i <= n; i++) {
		while (ll < n && b[ll+1] < a[i]) ++ll;
		l[i] = ll;
	}
	
	f[0][0] = 1;
	for (int i = 1; i <= n; i++) {
		f[i][0] = f[i-1][0];
		for (int j = 1; j <= i; j++)
			f[i][j] = (1ll*f[i-1][j]+1ll*f[i-1][j-1]*max(0ll, l[i]-j+1)%P)%P;
	}
	for (int i = 0; i <= n; i++) g[i] = 1ll*f[n][i]*fac[n-i]%P;
	int ans = 0;
	for (int i = k; i <= n; i++)
		if ((i-k)&1) (ans -= 1ll*C(i, k)*g[i]%P) %= P;
		else (ans += 1ll*C(i, k)*g[i]%P) %= P;
	cout<<(ans+P)%P<<endl;
}
posted @   Star_F  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示