组合数学(排列 组合 容斥 二项式 母函数)

组合数学

排列

A a b = a ! ( a − b ) ! A_a^b = \frac{a!}{(a - b)!} Aab=(ab)!a!

可重复排列数

从 n 个物品可重复的取k个排列数为 n k n^k nk

错排问题

n封不同的信, 编号分别是 1, 2, 3, 4, 5 现在要把五封信放在编号1, 2, 3, 4, 5的信封中, 要求信封的编号和信编号不同, 有多少种不同的放置方法

n个人将各自的帽子混在一起后任取一项,求恰有k个人拿对自己的帽子的概率:

错排问题的定义如下:

给定n元素集合X,它的每一个元素都有一个特定的位置,而现在要求求出集合X的排列中没有一个元素在它指定位置上的排列的数目.

用 D n 表 示 1 , 2 , . . . , n 错 排 数 目 , 根 据 容 斥 原 理 可 以 得 到 D n 的 公 式 ( 证 明 见 《 组 合 数 学 》 第 五 版 108 页 ) 用D_n表示{1,2,...,n} 错排数目,根据容斥原理可以得到D_n的公式(证明见《组合数学》第五版108页) Dn1,2,...,n,Dn(108)

D n = n ! ( 1 − 1 / 1 ! + 1 / 2 ! − 1 / 3 ! + . . . + ( − 1 ) n ∗ 1 / n ! ) D_n = n!(1-1/1!+1/2!-1/3!+...+(-1)^n*1/n!) Dn=n!(11/1!+1/2!1/3!+...+(1)n1/n!)

还可以通过
D 1 = 0 , D 2 = 1 D_1=0, D_2=1 D1=0,D2=1
出发,递推得:

D n = ( n − 1 ) ( D n − 1 + D n − 2 ) ( D 1 = 0 , D 2 = 1 ) D_n = (n - 1)(D_{n-1} + D_{n - 2})\quad(D_1 = 0, D_2 = 1) Dn=(n1)(Dn1+Dn2)(D1=0,D2=1)

D n − m ∗ C n m 为 答 案 D_{n - m} * C_n^m 为答案 DnmCnm

不相邻排列

n 个自然数中选 k 个, 这 k 个数中任何两个数都不相邻的组合有 C n − k + 1 k C_{n-k+1}^k Cnk+1k

圆排列

有五个小朋友,手拉手排成一个圆做游戏,求不同的排法数?

n个人围成一圈, 所有排列数记为 Q n n Q_n^n Qnn , 考虑其中已经排好的一圈,从不同位置断开,又变成不同的队列。 所以有 Q n n ∗ n = A n n = ( n − 1 ) ! Q^n_n * n = A_n^n = (n - 1)! Qnnn=Ann=(n1)!

可知 n 个不同物品中取 m 个的圆排列数公式:
Q n r = A n r / r = n ! / ( r ∗ ( n − r ) ! ) Q_n^r=A_n^r/r = n!/(r*(n-r)!) Qnr=Anr/r=n!/(r(nr)!)

不尽相异元素全排列

如果 n 个元素里, 有 p 个元素相同, 又有 q 个元素相同…,又有r个元素相同 (p+q+… +r ≤n), 则它的所有排列种数为:
n ! p ! q ! . . . r ! \frac{n!}{p!q!...r!} p!q!...r!n!

多重集全排列

同理如果 a1 重复 n1次, a2 重复 n2次, …, ak 重复 nk 次, 全排列数为 n ! n 1 ! n 2 ! . . . n k ! \frac{n!}{n1!n2!...nk!} n1!n2!...nk!n! (n = n1 + n2 + … + nk)

组合

C a b ​ = a ! b ! ( a − b ) ! C_a^b​ = \frac{a!}{b!(a-b)!} Cab=b!(ab)!a!

递推求组合数:
C a b = C a − 1 b + C a − 1 b − 1 C_a^b = C_{a-1}^b + C_{a-1}^{b-1} Cab=Ca1b+Ca1b1
当 a 和 b 不大的时候可以预处理, 复杂度 O ( n 2 ) O(n^2) O(n2) 对 mod 取余

const int N =2010, mod = 1e9 + 7;
int c[N][N];
void init() // 预处理函数
{
    for(int i = 0; i < N; i++) // 注意是从0开始
        for(int j = 0;j <= i; j++)
            if(!j) c[i][j] = 1; // b = 0,组合数定义为1
    else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}

当 a 和 b 超过 1 0 4 10^4 104 则不能用递推预处理

显然对于乘法来说有了同余定理, 但是对除法没有, 所以用快速幂求逆元, 复杂度 O ( n l o g n ) O(nlog_n) O(nlogn)

fact[0] = infact[0] = 1;//注意初始化
for(int i = 1; i < N; i ++)
    fact[i] = (LL)fact[i - 1] * i % mod;//注意这里和下面的强制long long 转化。不然会爆int
for(int i = 1; i < N ; i++)
    infact[i] = (LL)infact[i - 1] * ksm(i, mod - 2, mod) %  mod;//快速幂求逆元

当 a 和 b 为longlong 类型 一般用卢卡斯定理求解
C a b = C a % p b % p ∗ C a / p b / p ( m o d p ) C_a^b = C_{a \% p}^{b\%p} * C_{a/p}^{b/p}(mod p) Cab=Ca%pb%pCa/pb/p(modp)

int C(int a,int b) {
    
    
    int res = 1;
    for(int i = a, j = 1; j <= b; j++, i--) {
    
    
        res = (LL)res * i % p;
        res = (LL)res * ksm(j, p - 2) % p;
    }
    return res;
}

LL lucas(LL a, LL b) {
    
    
    if(a < p && b < p ) return C(a, b);
    return (LL)C(a % p , b % p ) * lucas(a / p, b / p) % p; //及时mod,LL
}

组合数常用公式

  1. C n m = C n − 1 m + C n − 1 m − 1 C_n^m = C_{n-1}^m + C_{n-1}^{m-1} Cnm=Cn1m+Cn1m1
  2. C n m = C n n − m C_n^m = C_n^{n-m} Cnm=Cnnm
  3. C n m + 1 = ( n − m ) / ( m + 1 ) ∗ C n m C_n^{m+1} = (n-m)/(m+1) * C_n^m Cnm+1=(nm)/(m+1)Cnm

可重组合数

可重组合是一类组合,从非空集合 X={1,2,…,n} 中,每次取出 r 个元素,允许元素重复,且不计顺序,这种组合称为集合 X 的一个 r 可重组合,集合 X 的 r 可重组合的总数为
H n r = C n + r − 1 r = ( n + r − 1 ) ! r ! ( n − 1 ) ! H_n^r=C_{n+r-1}^{r}=\frac{(n+r-1)!}{r!(n-1)!} Hnr=Cn+r1r=r!(n1)!(n+r1)!
模型:

取 r 个无标志的球,n 个有区别的盒子,每个盒子允许放多于一个球,或者空盒

不相邻组合

不相邻的组合是指从 A = {1, 2, … n} 中取 r 个不相邻的数进行组合(不可重) 组合数为:
C n − r + 1 r C_{n-r+1}^{r} Cnr+1r
模型:

某保密装置须同时使用若干把不同的钥匙才能打开. 现有7人, 每人持若干钥匙. 须4人到场, 所备钥匙才能开锁

问1: 至少有多少把不同的锁?

答: 每三个人至少缺一把钥匙, 任意四个人不缺钥匙, 那么每三个人所缺的钥匙必须不同, 如果有两组缺的钥匙相同(abc 缺 x, abd 缺 x), 那么 abcd 还是缺 x, 因此答案为 C 7 3 C_7^3 C73

问2: 每人至少持几把钥匙?

答: 任意4个人都不缺钥匙, 任一人对于其他6人中的每3人, 都至少有1把钥匙这三人不同才能开锁。
所以每人至少 C 6 3 C_6^3 C63

字典序

839647521 的下一个字典序
从后往前找,第一个下降的的数是 4,与从后往前第一个大于它的数 5 交换变成 839657421,然后将 5 之后的数按照递增排列 (其实就是一个翻转) 即 839651247

代码

public static String nxtPermutation(String nums) {
    
    
    int len = nums.length();
    int first = 0, second = 0;
    boolean f = false;
    for(int i = len - 2; i >= 0; i--) {
    
    
        if(nums.charAt(i) < nums.charAt(i + 1)) {
    
    
            first = i;
            f = true;
            break;
        }
    }
    // 如果是降序排列
    if(!f) {
    
    
        return null;
    }
    for(int i = len - 1; i >= 0; i--) {
    
    
        if(nums.charAt(i) > nums.charAt(first)) {
    
    
            second = i;
            break;
        }
    }
    StringBuffer sb = new StringBuffer(nums);
    char tmp = sb.charAt(first);
    sb.setCharAt(first, sb.charAt(second));
    sb.setCharAt(second, tmp);
    for(int i = first + 1, j = len - 1; i <= j; i++, j--) {
    
    
        tmp = sb.charAt(i);
        sb.setCharAt(i, sb.charAt(j));
        sb.setCharAt(j, tmp);
    }
    return sb.toString();
}

找出字符串的最大字典序的子序列

最后一个字符肯定是最大子序列中的一个字符,然后假定最大的字符为 ‘a’,从字符串最后一个字符开始变量,若该字符大于或等于最大字符,则让该字符入栈,并且将该字符替换成最大的字符

找出字符串的最大字典序的子串

先找到最大字典序的字符, 然后用双指针标识两个最大的字符, 然后开始以两个指针为起点遍历, 如果遇到不同的判断大小, 满足条件就改变 begin 指针的位置, 最后得到的 begin 到 len 的字串就是所求

public static String getMaxSubString(String s) {
    
    
    int len = s.length();
    int bg = 0;
    for(int i = 1; i < len; i++) {
    
    
        if(s.charAt(i) > s.charAt(bg)) {
    
    
            bg = i;
        }
    }
    for(int i = bg + 1; i < len; i++) {
    
    
        if(s.charAt(i) == s.charAt(bg)) {
    
    
            for(int j = bg, k = i; j < len && k < len; j++, k++) {
    
    
                if(s.charAt(j) != s.charAt(k)) {
    
    
                    if(s.charAt(j) < s.charAt(k)) {
    
    
                        bg = i;
                    }
                    break;
                }
            }
        }
    }
    return s.substring(bg, len);
}

卡特兰数

对卡特兰数的初步理解:有一些操作,这些操作有着一定的限制,如一种操作数不能超过另外一种操作数,或者两种操作不能有交集等,这些操作的合法操作顺序的数量

卡特兰数(fn) 的前几项为:

1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790…
f n = f 0 ∗ f n − 1 + f 1 ∗ f n − 2 + . . . f n − 1 ∗ f 0 ( n > = 2 ) f_n = f_0 * f_{n-1} + f_1 * f_{n-2} + ... f_{n-1} * f_0 (n >= 2) fn=f0fn1+f1fn2+...fn1f0(n>=2)
递推关系:
f n = 4 n − 2 n + 1 f n − 1 f_n = \frac{4n-2}{n+1}f_{n-1} fn=n+14n2fn1
通项公式:
f n = 1 n + 1 C 2 n n f_n=\frac{1}{n+1}C_{2n}^n fn=n+11C2nn
经过化简后得:
f n = C 2 n n − C 2 n n − 1 f_n = C_{2n}^n - C_{2n}^{n-1} fn=C2nnC2nn1
只要我们在解决问题时得到了上面的一个关系,那么你就已经解决了这个问题,因为他们都是卡特兰数列

实际问题

  1. n 个元素的二叉查找树有多少种

  2. n * n棋盘从左下角走到右上角而不穿过主对角线的走法

  3. 2n 个人排队买票问题,票价 50,n 个人拿 50 元,n 个人拿 100元,售票处无零钱,能顺利卖票的所有排队方式

  4. n 个元素全部入栈并且全部出栈的所有可能顺序

  5. 你现在有 n 个 0 和 n 个 1 ,问有多少个长度为 2n 的序列,使得序列的任意一个前缀中 1 的个数都大于等于 0 的个数

  6. 你有 n 个左括号, n 个右括号,问有多少个长度为 2n 的括号序列使得所有的括号都是合法的

  7. 有一个栈,我们有 2n 次操作, n 次进栈, n 次出栈,问有多少中合法的进出栈序列

这些问题的答案都是卡特兰数F(n)。但是很明显可以看出后三个问题是同质的
都可以抽象成 2n 个操作组成的操作链,其中A操作和B操作各 n 个,且要求截断到操作链的任何位置都有:A操作(向右走一步、收到50元、元素入栈)的个数不少于B操作(向上走一步、收到100元找出50元、元素出栈)的个数

n <= 35 的卡特兰数

public static long f[] = new long[36];
public static void getCatalan() {
    
    
    f[0] = f[1] = 1L;
    for(int i = 2; i < 36; i++) {
    
    
        f[i] = 0;
        for(int j = 0; j < i; j++) {
    
    
            f[i] = f[i] + f[j] * f[i - j + 1];
        }
    }
}

n >= 35 的卡特兰数通过卢卡斯定理以及求解组合数

int C(int a,int b) {
    
    
    int res = 1;
    for(int i = a, j = 1; j <= b; j++, i--) {
    
    
        res = (LL)res * i % p;
        res = (LL)res * ksm(j, p - 2) % p;
    }
    return res;
}
LL lucas(LL a, LL b) {
    
    
    if(a < p && b < p ) return C(a, b);
    return (LL)C(a % p , b % p ) * lucas(a / p, b / p) % p; //及时mod,LL
}

容斥原理

计算几个集合并集的大小,先计算出所有单个集合的大小,减去所有两个集合相交的部分,加上三个集合相交的部分,再减去四个集合相交的部分,以此类推,一直计算到所有集合相交的部分

主要是用于求解逆问题

题意:给定整数n和r,求区间[1, r]中与n互素的数的个数

先求1 ~ r 中和m 不互质的个数

那么这个时候我们就需要筛m 的质因数,然后求1 ~ r 中和 m 的质因数能被整除的个数,但是有些数会被筛多次,所以我们需要求并集,然后减去这个并集,利用容斥原理,先求不互质的个数 ans,最后结果 n−ans。

先将n分解质因子。存到v 容器里。

假如n 有 2,3,5质因子,那么2, 3, 5的倍数与m 都不互质,但是会有重复。用容斥原理算出正确的即可。

r / 2 + r / 3 + r / 5 - r / (2 * 3) -r / (3 * 5) -r / (2 * 5) + r / (2 * 3 * 5)

出现奇数次的加,出现偶数次的减。

代码枚举所有质因子的组合时用二进制枚举。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll ans;
int slove(int n, int r) {
    
    
	vector<int> v;
	for(int i = 2; i * i <= n; i++) {
    
     // 筛互质数
		if(n % i == 0) {
    
    
			v.push_back(i);
			while(n % i == 0) {
    
    
				n /= i;
			}	
		}
	}
	if(n > 1) v.push_back(n); //这时候 n就也是一个质数,
	ll tmp, s = 0;
	for(int i = 1; i < (1 << v.size()); i++) {
    
    
		tmp = 1, s = 0;
		for(int k = 0; k < v.size(); k++) {
    
    
			if(i & (1 << k)) {
    
    
				tmp *= v[k];
				s++;
			}
		}
		if(s & 1) {
    
    
			ans += (ll)r / tmp;
		} else {
    
    
			ans -= (ll)r / tmp;
		}
	}
	return r - ans;
}
int main() {
    
    
	ios::sync_with_stdio(false);
	int n, r;
	cin >> n >> r;
	cout << slove(n, r);
	return 0;
}

题意:给出n个互质数ai和整数r。求在区间[1;r]中,至少能被一个ai整除的数有多少。

解决此题的思路和上题差不多,计算ai所能组成的各种集合(这里将集合中ai的最小公倍数作为除数)在区间中满足的数的个数,然后利用容斥原理实现加减。

此题中实现所有集合的枚举,需要2^n的复杂度,求解lcm需要O(nlogr)的复杂度。

如果是n 个整数的话就需要去掉非质数

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int a[10000000], ans, tmp;

int solve(int n, int r) {
    
    
	for(int i = 1; i < (1 << n); i++) {
    
    
		tmp = 1; int count = 0;
		for(int k = 0; k < n; k++) {
    
    
			if(i & (1 << k)) {
    
    
				tmp *= a[k];
				count++;
			}
		}
		ans += count & 1 ? (r / tmp) : -(r / tmp); 
	}
	return ans;
}
int main() {
    
    
	ios::sync_with_stdio(false);
	int n, r;
	cin >> n >> r;
	for(int i = 0; i < n; i++) {
    
    
		cin >> a[i];
	}
	cout << solve(n, r) << endl;
	return 0;
}

二项式定理

公式:
( x + y ) n = ∑ i = 0 n C n i x i y n − i (x + y)^n = \sum_{i=0}^{n}C_n^i{x^iy^{n-i}} (x+y)n=i=0nCnixiyni

例题: 你有一枚硬币,然后抛出,落在地上,正面朝上的概率是p/q。现在你扔k次,问你正面朝上的次数是偶数次的概率

很容易想到的是分母为 p k p^k pk 分子为 ∑ i = 0 k C k i ∗ q i ∗ ( p − q ) k − i ( i 为 偶 数 ) \sum_{i=0}^{k}C_k^i*q^i*(p-q)^{k-i}(i为偶数) i=0kCkiqi(pq)ki(i)

对应二项式定理, q 为 x, (p - q) 为 y

根据二项式偶数项相加 = ((x + y) ^ k + (x - y) ^k) / 2

所以最后快速幂 + 逆元解决, 其实对 / 2 处理一下就用不到逆元了 (

母函数

把组合问题的加法法则和幂级数的的乘幂的相加对应起来

母函数的思想很简单—就是把离散数列和幂级数一一对应起来,把离散数列间的相互结合关系对应成为幂级数间的运算关系,最后由幂级数形式来确定离散数列的构造

有1克、2克、3克、4克的砝码各一枚,能称出哪几种重量?每种重量各有几种可能方案?

那么接下来试试用这种表达式表示4个砝码的组合情况

(x^0 + x^1)* (x^0 + x^2) * (x^0 + x^3)* (x^0 + x^4)

= x^0 + x^1 + x^2 + 2x^3 + 2x^4 + 2x^5 + 2x^6 + 2x^7 + x^8+ x^9 + x^10

求用1g、2g、3g的砝码称出不同重量的方案数

大家把这种情况和第一种比较有何区别?第一种每种是一个,而这里每种是无限的。

怎样在母函数里表现出无限这种性质呢?很简单,我们以 2g 砝码为例,因为我们有无限个 2g 砝码,所以我们可以把 2 个 2g 砝码看成是 4g 砝码,3 个 2g 砝码看成是 6g 砝码,依次类推,以前两个砝码为例,那么多项式相应的就变成

(x^0 + x^1 + x^2 + x^3 + x^4 + x^5 + …… ) * (x^0 + x^2 + x^4 + x^6 + x^8+ x^10 + ……) * (x^0 + x^3 + x^6)

无限砝码情况下的某种重量的方案数 (和上面的样例不同, 这里每一种克都有且无限)

public class Main {
    
    
    public static final int N = 1000;
    public static int[] sup = new int[N];
    public static int[] tmp = new int[N];

    public static void main(String[] args) {
    
    
        InputReader in = new InputReader();
        int target = 0;
        while(in.hasNext()) {
    
    
            target = Integer.parseInt(in.next());
            // 初始化
            // 此时 sup 是用 1g 砝码的多项式, 如果没给 1g 的砝码就不是 i++
            // 而是 i += 砝码的重量
            Arrays.fill(sup, 1);
            Arrays.fill(tmp, 0);
            // 生成后续的第 i 个多项式, 从 2g 的砝码开始
            for(int i = 2; i <= target; i++) {
    
    
                // 遍历当前结果的第 j 项与第 i 个相乘
                for(int j = 0; j <= target; j++) {
    
    
                    for(int k = 0; k + j <= target; k += i) {
    
    
                        // 幂运算
                        tmp[j + k] += sup[j];
                    }
                }
                for(int j = 0; j <= target; j++) {
    
    
                    // 临时结果覆盖当前结果, 同时初始化 tmp
                    sup[j] = tmp[j];
                    tmp[j] = 0;
                }
            }
            System.out.println(sup[target]);
        }

    }

    static class InputReader {
    
    
        StringTokenizer tok = null;
        BufferedReader br = null;
        InputReader() {
    
    
            br = new BufferedReader(new InputStreamReader(System.in));
        }
        public boolean hasNext() {
    
    
            while(tok == null || !tok.hasMoreElements()) {
    
    
                try {
    
    
                    tok = new StringTokenizer(br.readLine());
                } catch (Exception e) {
    
    
                    return false;
                }
            }
            return true;
        }

        public String next() {
    
    
            return hasNext() ? tok.nextToken() : null;
        }
    }
}

二维母函数模板

1, 5, 10, 25, 50 五种硬币, 和一个价值 n, 问由硬币总数不超过 100 的方案有多少种

只需要在传统的的母函数上加上一维,来对数量加以限定

public class Main {
    
    
    public static final int N = 1000;
    public static int[][] sup = new int[N][N]; // 二维来处理数量
    public static int[][] tmp = new int[N][N];
    public static int[] num = new int[]{
    
    0, 1, 5, 10, 25, 50};
    public static int[] count = new int[6];
    public static void main(String[] args) {
    
    
        InputReader in = new InputReader();
        int target = 0;
        while(in.hasNext()) {
    
    
            target = Integer.parseInt(in.next());
            Arrays.fill(sup, 0);
            Arrays.fill(tmp, 0);
            // 0 也是一种方案
            if (target == 0) {
    
    
                System.out.println(1);
                continue;
            }
            for(int i = 0; i <= 100; i++) {
    
    
                sup[i][i] = 1;
            }
            // 每种金额的最大数量
            for(int i = 1; i <= 5; i++) {
    
    
                count[i] = target / num[i];
            }
            for(int i = 1; i <= 5; i++) {
    
    
                for(int j = 0; j <= target; j++) {
    
    
                    for(int k = 0; j + k <= target && k <= num[i] * count[i]; k += num[i]) {
    
    
                        // 处理数量
                        for(int l = 0; l + k / num[i] <= 100; l++) {
    
    
                            tmp[j + k][l + k / num[i]] += sup[j][l];
                        }
                    }
                }
                for(int j = 0; j <= target; j++) {
    
    
                    for(int l = 0; l <= 100; l++) {
    
    
                        sup[j][l] = tmp[j][l];
                        tmp[j][l] = 0;
                    }
                }
            }
            int ans = 0;
            for(int i = 0; i <= 100; i++) {
    
    
                ans += sup[target][i];
            }
            System.out.println(ans);
        }
    }

    static class InputReader {
    
    
        StringTokenizer tok = null;
        BufferedReader br = null;
        InputReader() {
    
    
            br = new BufferedReader(new InputStreamReader(System.in));
        }
        public boolean hasNext() {
    
    
            while(tok == null || !tok.hasMoreElements()) {
    
    
                try {
    
    
                    tok = new StringTokenizer(br.readLine());
                } catch (Exception e) {
    
    
                    return false;
                }
            }
            return true;
        }

        public String next() {
    
    
            return hasNext() ? tok.nextToken() : null;
        }

    }
}

指数型母函数

用于求解多重集的排列问题

n个元素,其中a1, a2, ……, an互不相同,进行全排列,可得 n! 个不同的排列

若其中某一元素 ai 重复了 ni 次, 全排列出来必有重复元素,其中真正不同的排列数应为 n ! n i ! \frac{n!}{ni!} ni!n!, 重复度为 ni!

同理如果 a1 重复 n1次, a2 重复 n2次, …, ak 重复 nk 次, 全排列数为 n ! n 1 ! n 2 ! . . . n k ! \frac{n!}{n1!n2!...nk!} n1!n2!...nk!n!

若只对其中的 r 个元素进行排列, 就用到了指数型母函数

G e ( x ) = a 0 + a 1 1 ! x + a 2 2 ! x 2 . . . G_e(x)=a_0 + \frac{a_1}{1!}x + \frac{a_2}{2!}x^2 ... Ge(x)=a0+1!a1x+2!a2x2...

物品 n 种, 每种数量分别为 k1, k2, …kn个, 求从中选出 m 个物品的排列方法数

构造母函数:

因为对于第一个元素可取的个数的范围是{0, 1, 2, 3…k1} 所以其指数型生成函数为 ( 1 + x 1 ! + x 2 2 ! . . . x k 1 k 1 ! ) (1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k1}}{k1!}) (1+1!x+2!x2...k1!xk1)

每一个元素的通项: x k / k ! x^k / k! xk/k!

同理:
G ( x ) = ( 1 + x 1 ! + x 2 2 ! . . . x k 1 k 1 ! ) ( 1 + x 1 ! + x 2 2 ! . . . x k 2 k 2 ! ) . . . ( 1 + x 1 ! + x 2 2 ! . . . x k n k n ! ) = a 0 + a 1 x + a 2 2 ! x 2 . . . a p p ! x p ( p = k 1 + k 2.. k n ) G(x)=(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k1}}{k1!})(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k2}}{k2!})...(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{kn}}{kn!}) =a_0 + a_1x + \frac{a_2}{2!}x^2 ...\frac{ap}{p!}x^p (p=k1+k2..kn) G(x)=(1+1!x+2!x2...k1!xk1)(1+1!x+2!x2...k2!xk2)...(1+1!x+2!x2...kn!xkn)=a0+a1x+2!a2x2...p!apxp(p=k1+k2..kn)
G(x)含义:ai 为选出 i 个物品的排列方法数

若题中有限定条件,只要把第i项出现的列在第i项的式中,未出现的不用列入式中
如: 物品 i 出现次数为非 0 偶数, 则原式改为 x^2 / 2! + x^4 / 4! + … + x^(ki / 2 * 2) / (ki / 2 * 2)!

例题:

有1,2,3,4四个数字组成的五位数中要求
数 1 出现次数不超过 2 次,但不能不出现;
2 出现次数不超过 1 次;
3 出现次数最多 3 次,可以不出现;
4 出现次数为偶数 (0, 2, 4)

求满足上述条件的数的个数:

根据可选数的范围求出每一个数字的指数型生成函数, 然后相乘, 化简

把最后得到的式子展开, 找到 x 5 x^5 x5 这一项, 转换为 x k / k ! x^k / k! xk/k! 的形式 (上下同时乘以5!..), 然后其系数为所求

模板题

有 n 种物品,每种物品有 ki 件,从中选出 m 件的排列数

public class Main {
    
    
    public static final int N = 1000;
    public static double[] sup = new double[N];
    public static double[] tmp = new double[N];
    public static int[] fact = new int[15];
    public static int[] num = new int[N];

    public static void main(String[] args) {
    
    
        InputReader in = new InputReader();
        fact[0] = 1;
        for(int i = 1; i <= 10; i++) {
    
    
            fact[i] = i * fact[i - 1];
        }
        int n, m;
        n = Integer.parseInt(in.next());
        m = Integer.parseInt(in.next());
        for(int i = 1; i <= n; i++) {
    
    
            num[i] = Integer.parseInt(in.next());
        }
        Arrays.fill(tmp, 0.0);
        Arrays.fill(sup, 0.0);
        for(int i = 1; i <= num[1]; i++) {
    
    
            // 初始化系数
            sup[i] = (1.0 / fact[i]);
        }

        for(int i = 2; i <= n; i++) {
    
    
            for(int j = 0; j <= m; j++) {
    
    
                for(int k = 0; j + k <= m && k <= num[i]; k++) {
    
    
                    // 和普通母函数不同, 这里的分母阶乘是系数
                    tmp[j + k] += (sup[j] / fact[k]);
                }
            }
            for(int j = 0; j <= m; j++) {
    
    
                sup[j] = tmp[j];
                tmp[j] = 0.0;
            }
        }

        int ans = (int)(sup[m] * fact[m] + 0.5);
        System.out.println(ans);
    }
    static class InputReader {
    
    
        StringTokenizer tok = null;
        BufferedReader br = null;
        InputReader() {
    
    
            br = new BufferedReader(new InputStreamReader(System.in));
        }
        public boolean hasNext() {
    
    
            while(tok == null || !tok.hasMoreElements()) {
    
    
                try {
    
    
                    tok = new StringTokenizer(br.readLine());
                } catch (Exception e) {
    
    
                    return false;
                }
            }
            return true;
        }

        public String next() {
    
    
            return hasNext() ? tok.nextToken() : null;
        }

    }
}

[;

猜你喜欢

转载自blog.csdn.net/qq_45560445/article/details/121589011