组合数学学习笔记(入门版)

排列组合

例1

NOIP2016 组合数问题

首先有排列组合公式 \(C_n^m = C_{n-1}^m + C_{n-1}^{m-1}\),根据这个递推出 \(C_n^m\) 是否可以被k整除,再用二维前缀和预处理出答案即可。

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;        
const int mod = 1e9 + 7, N = 2e3 + 10;
int t, k;
int n, m;
int C[N][N];
int sum[N][N];  //前缀和

inline int add(int a, int b) {
    if(a + b >= k) return a + b - k;
    else return a + b;
}

int main() {
    scanf("%d%d", &t, &k);
    n = m = 2000;
    for(int i = 1; i <= n; i++) C[i][0] = 1;
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                if(i == 1 && j == 1) C[i][j] = 1;
                else C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
                // cout << i <<' '<<j<<' '<<C[i][j]<< endl; ///
                sum[i][j] = 0;
            }
        }
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                if(C[i][j] == 0 && i >= j) sum[i][j] = 1;
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + sum[i][j] - sum[i - 1][j - 1];
                // cout << i <<' '<<j<<' '<<sum[i][j]<< endl; ///
            }
        }
    while(t--) {
        scanf("%d%d", &n, &m);
        printf("%d\n", sum[n][m]);
    }
    system("pause");
    return 0;
}

组合计数(包含容斥原理)

例1

\(x_1 + x_2+x_3\leq A\) 的非负整数解的组数(\(A \leq 1e9\)

方法1:

\(ans = \sum x_1 + x_2+x_3\leq i (i\leq A)\) 的方案数 会tle

方法2:

\(ans = \sum x_1 + x_2+x_3+x_4 = A\) 的方案数 OK!!

例2

方程 \(x_1+x_2+x_3=13\) 有多少组解? 其中 \(x_i < 6\)

——容斥原理

答案 = 全集 - (恰好满足每个\(x_i < 6\) 出现一次的方案数)

\(ans = C_{15}^2 - (3*C_9^2-3*C_3^2+0)\)

一般情况见例4

例3

\(x_1+x_2+...+x_n=m\),满足\(0\leq x_i \leq k\) 方案数

ans = \(C_{n + m - 1}^{n-1}\) 减去 不合法情况(\(x_i \geq k + 1\)

不合法情况可以根据容斥原理算出:\(C_n^1 * C_{n+m-1-(k+1)}^{n-1}-C_n^2 * C_{n+m-1-2*(k+1)}^{n-1}+...\)

时间复杂度 \(O(n)\)

例4

\(x_1+x_2+...+x_n=m\),满足最大的 \(x_i\) 刚好是 \(k\) 的方案数

ans = {\(x_i \leq k\)} 的方案数 减去 {\(x_i \leq k-1\)} 的方案数

用两次容斥原理即可。

例题:2021 CCPC 威海 M

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10, mod = 998244353;
int n, m, k;
ll fac[N], inv[N];

ll quickpow(ll a, ll x) {  //底数也开ll  因为可能是inv[] 
	ll ans = 1;
	while(x) {
		if(x & 1) {
			ans *= a;	
			ans %= mod;
		}
		a *= a;
		a %= mod;
		x /= 2;
	}
	return ans;
}

void init(int n) {
	fac[0] = 1;
	for(int i = 1; i <= n; i++) {
		fac[i] = fac[i - 1] * i % mod;
	}
	inv[n] = quickpow(fac[n], mod - 2);  
	for(int i = n - 1; i >= 1; i--) {
		inv[i] = inv[i + 1] * (i + 1) % mod;  
	}
	inv[0] = 1;
}

ll C(int n, int m) {
    if(n < m) return 0;
    return fac[n] * inv[m] % mod * inv[n - m] % mod;
}

inline ll solve(int k) {
    ll ans = C(n, n - m);
    for(int i = 1; i * k <= min(n, m); i++) {
        if(i % 2) ans = (ans - C(n - m + 1, i) * C(n - k * i, n - m) % mod + mod) % mod;
        else ans = (ans + C(n - m + 1, i) * C(n - k * i, n - m) % mod) % mod;
    }
    return ans;
}

int main() {
    init(1e5);
    scanf("%d%d%d", &n, &m, &k);
    if(k == 0) {
        if(m == 0) puts("1");
        else puts("0");
        system("pause");
        return 0;
    }
    if(n < m || m < k) {
        puts("0");
        system("pause");
        return 0;
    }
    printf("%lld\n", (solve(k + 1) - solve(k) + mod) % mod);
    system("pause");
    return 0;    
}
例5

HAOI2008 硬币购物

题意

\(n\) 种物品,给出它们的价值,\(t\) 组询问,每次给出这 \(n\) 种物品的数量,和想购买东西的价值。问要想买到这些东西,共有多少种方案数?

题解

假设没有数量的限制,那么就是一个完全背包问题

再根据容斥原理(不合法按奇加偶减)减去数量超过的方案数就是最终的答案

点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10;
int n, m, k, q, s;
int c[5], d[5];
ll f[N];  //价值为i的方案数

void init() {    // 完全背包
    f[0] = 1;
    for(int i = 0; i < 4; i++) {
        for(int j = c[i]; j <= 100000; j++) {
            f[j] += f[j - c[i]];
        }
    }
}

inline ll solve() {
    ll ans = 0;
    for(int S = 0; S < (1 << 4); S++) {
        int cnt = __builtin_popcount(S);
        ll tem = 0;
        for(int j = 0; j < 4; j++) {
            if(S & (1 << j)) {
                tem += 1ll * (d[j] + 1) * c[j];
            }
        }
        if(s < tem) continue;
        if(cnt % 2) ans -= f[s - tem];
        else ans += f[s - tem];
    }
    return ans;
}

int main() {
    for(int i = 0; i < 4; i++) cin >> c[i];
    init();
    cin >> q;
    while(q--) {
        for(int i = 0; i < 4; i++) cin >> d[i];
        cin >> s;
        printf("%lld\n", solve());
    }
    system("pause");
    return 0;    
}

生成函数

普通型生成函数

可以理解成 有无限项的数列 的系数

母函数 $O(n^2)$ 模板
#include<cstdio>
using namespace std;
const int N = 1020;
int c1[N],c2[N];
int w[N];
int n;
int main(){
	while(scanf("%d",&n)==1&&n){
		for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
		for(int i=0;i<=n;i++){
			c1[i]=0,c2[i]=0;
		}
		for(int i=0;i<=n;i++){
			c1[i]=1; 
		}
		for(int i=2;i<=n;i++){
			for(int j=0;j<=n;j++){
				for(int k=0;j+k<=n&&k<=w[i];k++){
					c2[j+k]+=c1[j];
				}
			}
			for(int k=0;k<=n;k++){
				c1[k]=c2[k],c2[k]=0;
			}
		}
		printf("%d\n",c1[n]);
	}
	return 0;
}



公式

· $\frac{1}{1 - x} = \sum_{i=0}^∞ x^i $

所以 $\frac{1}{(1 - x)^n} $ 中 \(x ^ k\) 的系数是 \(C_{n + k - 1} ^ {n - 1}\) = \(C_{n + k - 1} ^ {k}\)
(可以用类似 组合数学 \(y_1+y_2+...+y_n = k (y_i\geq 0)\) 求方案数 来表示)

指数型生成函数

在数列的基础上,\(x^i\) 还要除以 \(i!\) ,表示排列和顺序无关

指数型母函数 $O(n^2)$ 模板
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 15, mod = 1e9 + 7;
int n, m;
int num[N];
int f[N];
double c1[N], c2[N];

int main() {
    f[0] = 1;
    for(int i = 1; i <= 10; i++) f[i] = f[i - 1] * i;
    while(scanf("%d%d", &n, &m) == 2) {
        for(int i = 1; i <= n; i++) scanf("%d", &num[i]);
        for(int i = 0; i <= n; i++) {
            c1[i] = 0;
            c2[i] = 0;
        }
        for(int i=0;i<=num[1];i++){
			c1[i] = 1.0 / f[i]; 
		}
        for(int i=2;i<=n;i++){
			for(int j=0;j<=10;j++){
				for(int k=0;j+k<=10&&k<=num[i];k++){
					c2[j+k] += 1.0 * c1[j] / f[k];
				}
			}
			for(int k=0;k<=10;k++){
				c1[k]=c2[k], c2[k]=0;
			}
		}
        printf("%.0f\n", c1[m] * f[m]);
    }
    system("pause");
    return 0;
}

鸽巢原理

例1

POJ 3370

题意

给你 \(n\) 个数,你需要找到这些数中的一些,满足这些数的和是 \(c\) 的倍数,并输出这些数的下标

题解

乍一看感觉好像是 \(O(n^2)\) 的,但是并不需要,只要预处理出前缀和数组

如果 \(sum[i] \mod c == 0\) ,那么直接输出 \([1,i]\) 即可;如果 \(sum[i] \mod c == sum[j] \mod c\),输出 \([i + 1,j]\) 即可

posted @ 2022-10-24 11:29  starlightlmy  阅读(91)  评论(0)    收藏  举报