组合基本概念

基本计数原理:

分类计算加法原理,分布计算乘法原理。

简单容斥与摩根定理:

  • |AB|=|A|+|B||AB|
  • |ABC|=|A|+|B|+|C||AB||AC||CB|+|ABC|

|i=1nAi|=J{1,,n}(1)|J|+1|jJAj|

代码可以利用 dfs 或状压实现

 

摩根定理: 交集的补等于补集的并,并集的补等于补集的交。

AB=AB AB=AB

|i=1nAi|=J{1,,n}(1)|J||jJAj||i=1nAi|=J{1,,n}(1)|J||jJAj|

 

组合计数:

排列数:

n 个不同元素中依次取出 m 个元素排成一列,产生的不同排列的数量为:(取 m 个,将 m 个排序)

Anm(Pnm)=n!(nm)!

组合数:

n 个不同元素中取出 m 个组成一个集合(不考虑顺序),产生的不同集合的数量为:

Cnm=(nm)=n!m!(nm)!

 

性质:

  • Ank=Cnk×k!
  • Cnm=Cnnm
  • Cnm=Cn1m+Cn1m1 (杨辉三角)
  • kn×Cnk=Cn1k1
  • i=0nCni=2n
  • i=0n(1)iCni=0

 

组合数求解:

单个组合数 O(n)

代码
int C(int n, int k) {
	int p = 1, q = 1;
    for (int i = n - k + 1; i <= n; ++i)
        p *= i;
    for (int i = 1; i <= k; ++i)
        q *= i;
    return p / q;
}

Cij(i[0,n1],j[1,i]) 递推 O(n2)

代码
for (int i = 0; i < n; ++i) {
    C[i][0] = 1;
    for (int j = 1; j <= i; ++j) {
        C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
    }
}

 

二项式定理:

(a+b)n=k=0nCnkakbnk(ab)n=k=0n(1)kCnkakbnk

 

高精组合数:

Cnm 的值,结果可能很大,没有模数。

sol:
Cnm=n!m!×(nm)!
所以可以对 n! 阶乘进行质因数分解为 pici (方法在基础数论博客里)
然后对 m!(nm)! 分别进行质因数分解,每次使 ci 减一。
最后统计结果。
时间复杂度 O(n) (高精另算)

代码
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;


const int N = 5010;

int primes[N], cnt;
int sum[N];
bool st[N];


void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p) // 求n!中 质因子 p 需要累乘的次数 
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}


int main()
{
    int a, b;
    cin >> a >> b;

    get_primes(a);//用欧拉筛筛出 [1~a] 范围内的质数 

    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p); // C(a,b) 中第 i 个质数需要累乘的次数 
    }

    vector<int> res;
    res.push_back(1);

    for (int i = 0; i < cnt; i ++ ) // 枚举质因子 
        for (int j = 0; j < sum[i]; j ++ ) // 枚举当前质因子的个数 
            res = mul(res, primes[i]); // 做高精度乘低精度 

    for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
    puts("");

    return 0;
}

 

多重集排列数:

k 种元素,有 n1a1n2a2nkak 。有多少种排列方案。

(n1+n2++nk)!n1!×n2!××nk!

多重集组合数:

1.
k 种元素,有 n1a1n2a2nkak 。从中取出 r ( i[1,k],rni )个的方案数。

r 个元素构成的集合是 {x1·a1,x2·a2,,xk·ak}
x1+x2++xk=rxi0 (隔板法)
答案为 Cr+k1k1

2.
k 种元素,有 n1a1n2a2nkak 。从中取出 r ( ri=1kni )个的方案数为:

Ck+r1k1i=1kCk+rni2k1+1i<jkCk+rninj3k1+(1)kCk+ri=1kni(k+1)k1

若先不考虑 ni 的限制,从 S={·a1,·a2,,·ak} 中取出 r 个元素,则方案数为 Ck+r1k1
Si 表示包含至少 ni+1ai 的多重集,即从 S 中先取出 ni+1ai, 然后再任选 rni1 个元素构成 Si ,可得不同的 Si 的数量为 Ck+rni2k1
那么进一步思考,从 S 中先取出 ni+1ainj+1aj ,然后再任选 rninj2 个元素,构成的集合即为 SiSj ,方案数为 Ck+rninj3k1
根据容斥原理可得:

|i=1kSi|=i=1kCk+rni2k11i<jkCk+rninj3k1++(1)k+1Ck+ri=1kni(k+1)k1

故答案为 Ck+r1k1|i=1kSi|

板子题Devu and Flowers

代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;

inline ll read(){
	ll s=0,k=1;
	char c=getchar();
	while(c>'9'||c<'0'){
		if(c=='-')k=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9'){
		s=(s<<3)+(s<<1)+(c^48);
		c=getchar();
	}
	return s*k;
}

const int mod=1e9+7;
ll a[25],n,m,ans,inv[25];

ll ksm(ll a,ll b){
	ll t=1;
	for(;b;b>>=1,a=a*a%mod)
		if(b&1) t=t*a%mod;
	return t;
}

ll C(ll y,ll x){
	if(x<0||y<0) return 0;
	if(x>y) return 0;
	y%=mod;
	ll ans=1;
	for(int i=0;i<x;i++) ans=ans*(y-i)%mod;
	(ans*=inv[x])%=mod;
	if(y==0) return 1;
	return ans;
}

int main(){
	ll t=1;
	for(int i=1;i<=20;i++) t=t*i%mod;
	inv[20]=ksm(t,mod-2);
	for(int i=19;i>=0;i--) inv[i]=inv[i+1]*(i+1)%mod;
	n=read();m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int j=0;j< 1<<n ;j++){
		if(j==0) ans=(ans+C(n+m-1,n-1))%mod;
		else{
			ll tt=n+m;
			ll s=0;
			for(int i=0;i<n;i++)
				if(j>>i&1){
					s++;
					tt-=a[i+1];
				}
			tt-=s+1;
			if(s&1) ans=(ans-C(tt,n-1))%mod;
			else ans=(ans+C(tt,n-1))%mod;
		}
	}
	printf("%lld\n",(ans+mod)%mod);
	return 0;
}

 

 

例题:

数字求和:

f(x)x 在十进制下的各位数字之和,例如 f(123)=1+2+3=6 ,求

i=010nf(x)

答案对 109+7 取模。

考虑除去 10n 这一个数字之外的其他数字中:
i[1,9] ,都会在每一位上出现 10n1 次。
所以这些数字对答案的贡献就是 i=19i×n×10n1=45×n×10n1
故答案为 45×n×10n1+1

 

圆盘染色:

n 块组成一个圆盘,用 m 种颜色染色,使得相邻两块的颜色不同。求方案数。

考虑如果 n 的颜色与 n2 相同,则将 n2,n1,n 看作一个整体。
那么 n1 块有 m1 种选择,则方案数为 (m1)×f(n2) ,( f(i) 表示分成 i 块的方案数)
考虑如果 n 的颜色与 n2 不同,将 n2,n1,n 看作一个整体。
那么 n1 块有 m2 种选择,则方案数为 (m2)×f(n1)
f(n)=(m1)×f(n2)+(m2)×f(n1)
( f(1)=0,f(2)=m×(m1),f(3)=m×(m2) )

 

数三角形

给定一个 N×M 的网格,请计算三点都在格点上的三角形共有多少个。注意三角形的三点不能共线。

【三角形数量】等于【任选三个点的方案数】减去【三点共线的方案数】
【三点共线的方案数】等于【横着的】加【竖着的】加【斜着的】
【横着的】:(m+1)×Cn+13
【竖着的】:(n+1)×Cm+13
斜着的直线按斜率正负分为两种,并且这两种的方案数是相等的。因此,我们只需要计算出斜率为正的方案数,再乘以 2 即可。将核心计算内容——斜率为正的方案数记为 ans
假设网格中 AB 平行于底边 x 轴,长度为 iBC 平行于侧边 y 轴,长度为 j ,那么 AC 上的整点数量为 gcd(i,j)1 (不算 A,C 两点)。

ans=i=1nj=1m(ni+1)(mj+1)(gcd(i,j)1)

此时还能继续化简:

=i=1nj=1m(ni+1)(mj+1)(dgcd(i,j)φ(d)1)=i=1nj=1m(ni+1)(mj+1)dgcd(i,j)d1φ(d)=d=2min(n,m)φ(d)i=1nd(nid+1)j=1md(mjd+1)=14d=2min(n,m)φ(d)(nd+n%d+2)nd(md+m%d+2)md()

至此可以 O(n) 求得答案。

 

Counting swaps

给定你一个 1n 的排列 p ,可进行若干次操作,每次选择两个整数 x,y ,交换 px,py 。用最少的操作次数将给定排列变成单调上升的序列 1,2,,n ,有多少种方式呢?

对于一个排列 p1,p2,,pn ,每一个 pii 连一条无向边,构成一张由若干环组成的无向图,目标状态即为 n 个自环。

f[i] 表示一个大小为 i 的环,在保证交换次数最少的情况下,有多少种方法将其变成目标状态。

每一次交换可以把大小为 i 的环拆成大小为, x,y 的两个环(x+y=n) 。设 T(x,y) 表示有多少种交换方法可以将一个大小为 i 的环拆成两个大小分别为 x,y 的环。可以发现:当 x=y 时, T(x,y)=x ,否则 T(x,y)=x+y

x 环需要 x1 次交换达到目标状态,同理 y 环需要 y1 次操作,由于 x 环和 y 环的操作互不干扰且可以随意排列,因此得到转移方程:

f[i]=x+y=if[x]×f[y]×T(x,y)×(i2)!(x1)!×(y1)!

(在打表后可发现 f[n]=nn2)
假设排列中有 k 个大小为 L1,L2,,Lk 的环,则

Ans=(i=1kf[Li])×((nk)!i=1k(Li1)!)

posted @   programmingysx  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示