组合数学--排列组合

1. 概述

组合数学这是笔者在研究生阶段唯一的一门数学课了吧,希望做个了断。
组合数学可以理解成是离散数学中的一部分,广义的组合数学就是离散数学
离散数学可以理解成是狭义的组合数学和图论、代数结构、数理逻辑的统称
以上所说仅仅是叫法上的不同,总而言之组合数学是研究离散对象的科学,但是在计算机科学中有着重要的作用

1.1 应用

这里只提几个很出名的问题

  • 幻方问题
  • 四色问题
  • 最短网络
  • 最小生成树和最小斯坦纳树
  • 无尺度网络
  • 小世界网络

1.2 三大问题

  • 存在(Existence Problem)
  • 计数(Counting Problem)
  • 优化(Optimization Problem)

2. 排列组合

最基本的排列组合无需多言
计数问题最重要的是做到无重复无遗漏,即不重不漏

2.1 两大法则

  • 加法法则:分类问题
  • 乘法法则:分步问题

2.2 排列

  • 圆排列
    P(n,r)r,2rn \frac {P(n, r)} r, 2 \leq r \leq n
  • 项链排列
    P(n,r)r,3rn \frac {P(n,r)} r, 3 \leq r \leq n
  • 多重全排列
    • t种球,个数有限
    • 打标号
  • 多重全排列
    • 分类枚举
  • 可重排列
    nr n^r

3. 放球模型

  • 排列:n个不同的球中取r个,放入r个不同的盒子,每个盒子一个
    P(n,r)=n(n1)...(nr+1)=n!(nr)! P(n, r) = n*(n-1)*...*(n-r+1)=\frac {n!} {(n-r)!}
  • 组合:n个不同的球中取r个,放入r个相同的盒子,每个盒子一个
    C(n,r)=n!r!(nr)! C(n, r) = \frac {n!} {r!(n-r)!}

4. 模型转换

A事件不好计算,但是A和B一一对应,B好计算事件,可以转换为求B的,也就求出来了A的

100100人打乒乓球赛,每个选手至少打一局,输者淘汰,最后产生一名冠军,需要比赛多少场?
答:9999场,因为要选出来冠军,淘汰的选手和比赛一一对应
Cayley定理:n个有标号的顶点的树的数目是nn2n^{n-2},用n1n-1条边将1,2,...,n1,2,...,n连接起来的连通图的数目是nn2n^{n-2}

5. 线性方程的解

线性方程x1+x2+...+xn=bx_1+x_2+...+x_n = b的非负整数解的个数是C(n+b1,b)C(n+b-1, b)
【解释】相当于把一堆x分成b组,每组个数不限,

5.1 若干等式及其组合意义

  1. (0,0)(0,0)走到(m,n)(m,n)的方法数C(m+n,m)C(m+n, m)
  2. C(n,r)=C(n,nr)C(n, r) = C(n, n-r)
  3. C(n,r)=C(n1,r)+C(n1,r1)C(n, r) = C(n-1, r) + C(n-1, r-1)
    1a1a2...ann1\leq a_1 \leq a_2 \leq ... \leq a_n \leq n取整数,对取法分类
    - a1=1a_1 = 1,有C(n1,r1)C(n-1, r-1)种方案
    - a1>1a_1 \gt 1,有C(n1,r)C(n-1, r)种方案
  4. C(n+r+1,r)=C(n+r,r)+C(n+r1,r1)+...+C(n+1,1)+C(n,0)C(n+r+1, r) = C(n+r, r) + C(n+r-1, r-1) + ... + C(n+1, 1) + C(n, 0)
  5. C(n,l)C(l,r)=C(n,r)C(nr,lr)C(n,l) C(l, r) = C(n, r)C(n-r, l-r)
  6. C(m,0)+C(m,1)+...+C(m,m)=2m,m0C(m, 0) + C(m, 1) +...+C(m, m) = 2^m, m \geq 0
  7. C(n,0)C(n,1)+C(n,2)...±C(n,n)=0C(n,0) - C(n, 1)+C(n,2)-...\pm C(n,n) = 0
    • 6 和 7 都是(x+y)n(x+y)^n
  8. Vandemonde 恒等式C(m+n,r)=C(m,0)C(n,r)+C(m,1)C(n,r1)+...+C(m,r)C(n,0)C(m+n, r)=C(m,0)C(n,r)+C(m,1)C(n, r-1)+...+C(m, r)C(n, 0)

6. 全排列生成算法

这算是本章的重点了吧
全排列的个数是n!n!。现在的问题是

  1. 生成所有的排列,
  2. 根据某一个排列,计算之后或者之前第kk个排列是什么。

注意一点,因为是全排列,所以其含义包含着所有元素都不相同。

6.1 字典序法

经典的方法,就是按照从小到大枚举变化即可。
举例来说,12341234
1234,1243,1324,1342,1423,14322134,2143,2314,2341,2413,24313124,3142,3214,3241,3412,34214123,4132,4213,4231,4312,4321 1234, 1243, 1324, 1342, 1423, 1432\\ 2134, 2143, 2314, 2341, 2413, 2431\\ 3124, 3142, 3214, 3241, 3412, 3421\\ 4123, 4132, 4213, 4231, 4312, 4321\\
可以假设初始有一个向左的箭头,代表kk移动的方向,但是kk是否可以移动取决于在kk的方向上是否一个比kk小的数字存在。
字典序法想要的是这一个和下一个具有尽可能共同前缀,也即变化在尽可能后缀上。
在实际上理解的时候,从小到大的排列,就是把当前排列从右往左扫描,找到第一个下降的数字,并且把该数字和其后数字中比它大的最小的那一个交换,并把新的后续数字从小到大排列。例如:1321323322是上升的,1133是下降的,所以11就是要交换的那一个数字,把它和2323中较小的22交换,并把1313从小到大排列,132132下一个就是213213

6.1.1 序号

全排列的序号就是先于此排列的个数。

排列 123 132 213 231 312 321
序号 0 1 2 3 4 5

6.1.2 康拓展开

百度定义X=a[n](n1)!+a[n1](n2)!+...+a[i](i1)!+...+a[1]0!X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0!,其中,a[i]a[i]为整数,并且0a[i]<i,(1in)0\leq a[i]\lt i, (1 \leq i \leq n)

6.1.3 中介数

字典序的中介数代表的是当前数字右边比其小的数字的个数。计算当前排列后第kk个排列只要把当前中介数加上kk,然后再把新的中介数还原成排列数即是要求的排列数。
【注意】中介数和kk是不同的进制,要按照中介数的进制进行计算。
举例:839647521839647521,它的序号也即康拓展开式78!+27!+66!+45!+24!+33!+22!+11!7*8!+2*7!+6*6!+4*5!+2*4!+3*3!+2*2!+1*1!,其中7264232172642321就是中介数,7264232172642321代表的是在当前数字比其右边大的数字的个数。
7264232172642321推出839647521839647521
P1P2P3P4P5P6P7P8P9P_1 P_2 P_3 P_4 P_5 P_6 P_7 P_8 P_9

  1. 7+1=8P1=87+1=8→P_1 = 8
  2. 2+1=3P2=32+1=3→P_2=3
  3. 6+1=7,37P37+1=88P38+1=9P3=96+1=7,但3<7已在P_3左边出现,7+1=8,但8已在P_3左边出现,8+1=9→P_3=9
  4. 4+1=5,3<5P45+1=6P4=64+1=5,但3<5已在P_4左边出现,5+1=6→P_4=6
  5. 2+1=33P53+1=4P5=42+1=3,但3已在P_5左边出现,3+1=4→P_5=4
  6. 3+1=43,4P64+1+1=66P66+1=7P6=73+1=4,但3,4已在P_6左边出现,4+1+1=6,但6已在P_6左边出现,6+1=7→P_6=7
  7. 2+1=33P73+1=44P74+1=5P7=52+1=3,但3已在P_7左边出现,3+1=4,但4已在P_7左边出现,4+1=5→P_7=5
  8. 1+1=2P8=21+1=2→P_8=2
  9. P9=1P_9 = 1

中介数,序号和排列之间是一一对应的关系。
字典序下的对应关系
可用归纳法证明:k=1n1kk!=n!1\sum_{k=1}^{n-1}{k*k!} = n! - 1

6.2 递增进位制

递增进位制是不固定进制中的基数,从右向左数数,第ii个位置的数字逢i+1i+1进一位,即从右向左,逢2345...2、3、4、5、...进一位。
它的中介数是在ii的右边比ii小的数字的个数。但是它的中介数的进制和字典序的一致,都是从右向左,逢2345...2、3、4、5、...进一位。

6.3 递减进位制

递减进位制和递增进位制类似,它的中介数是把递增进位制逆置,但是它的中介数的进位是从左往右,逢2345...2、3、4、5、...进一位。

6.4 SJT邻位对换

它的方向是双向的,通过保存数字的“方向性“来快速得到下一个排列。
设定b2,b3,b4,b5,b6,b7,b8,b9b_2, b_3, b_4, b_5, b_6, b_7, b_8, b_9为我们要求的中介数。

  • 规定22的方向一定向左。b2b_2就是从22开始,背向22的方向所有比22小的数字的个数。
  • 对于每一个比22大的数字ii:
    • ii奇数,其方向性决定于bi1b_{i-1}的奇偶性,奇向右,偶向左
    • ii偶数,其方向性决定于bi1+bi2b_{i-1}+b_{i-2}的奇偶性,奇向右,偶向左

bib_i的值就是背向ii的方向直到排列边界这个区间里比ii小的数字的个数。
SJT方法的中介数进位同递减进位制。

6.5 总结

其实还有很多方法,老师说可以参考DonaldErvinKnuthDonald Ervin Knuth的神书《计算机程序设计的艺术》里面Permutation Generating。
上述方法是为了让新生成的排列和原排列的尽可能相似,就是换的数字尽可能少。

7. 代码实现

并没有很好的代码格式,只是为了应付OJ,又不会使用C++,所以凑合着使用了Java。其实本次OJ中C++的long long够用。

7.1 题目描述

给定一个11nn的排列PP,请求出这个排列根据某种顺序的后面第kk个排列。
输入格式

  • 第一行是三个由空格隔开的整数nn,typetype,kk
  • 第二行是nn个由空格隔开的[1,...,n][1, ..., n]中的无重复整数,表示一个排列;行末可能会有空格。

typetype的含义如下:

  • type=1type = 1时,请按字典序计算;
  • type=2type=2时,请按递增进位制计算;
  • type=3type=3时,请按递减进位制计算;
  • type=4type=4时,请按邻位对换法的顺序计算。

k<0k<0时,请计算根据顺序的前面第−k个排列。

输出格式
第一行输出nn个由单个空格隔开的整数,表示答案排列。
样例1
Input
9 1 1
8 3 9 6 4 7 5 2 1
Output
8 3 9 6 5 1 2 4 7
样例2
Input

9 2 1
8 3 9 6 4 7 5 2 1
Output
8 4 9 6 1 7 5 2 3
样例3
Input

9 3 1
8 3 9 6 4 7 5 2 1
Output
8 9 3 6 4 7 5 2 1
样例4
Input

9 4 1
8 3 9 6 4 7 5 2 1
Output
8 3 6 9 4 7 5 2 1
数据规模
kk的取值保证答案存在。

存在以下几种限制:

  • 1n101≤n≤101n201≤n≤20
  • k>0k>0k<0k<0
  • type=1,2,3,4type=1, 2, 3, 4

对于每一种限制组合,都有一个测试点,共2×2×4=162×2×4=16个测试点。

时空限制
时间限制:1000ms1000ms 内存限制:512MiB512MiB

7.2 代码实现

import java.util.Scanner;

public class ShiftingPermutations {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);

		int n = sc.nextInt();
		int type = sc.nextInt();
		long k = sc.nextLong();
		long[] nums = new long[n];

		for (int i = 0; i < n; ++i)
			nums[i] = sc.nextLong();

		switch (type) {
		case 1:
			// 请按字典序计算;
			dictOrder(nums, n, k);
			break;
		case 2:
			// 请按递增进位制计算;
			incrementalCarry(nums, n, k);
			break;
		case 3:
			// 请按递减进位制计算;
			degressiveCarry(nums, n, k);
			break;
		case 4:
			// 请按邻位对换法的顺序计算。SJT
			orthoSubstitution(nums, n, k);
			break;
		default:
			throw new UnsupportedOperationException("Unsupported Number:" + type);
		}

		printArr(nums);
	}

	// 打印数组
	private static void printArr(long[] nums) {
		for (long num : nums) {
			System.out.print(num + " ");
		}
		System.out.println();
	}

	// 原排列 -> 中介数
	private static long[] permutation2Mid(long[] nums, int n) {
		long[] midNums = new long[n - 1];
		// 统计i位置右边小于i的个数
		for (int i = 0; i < n; ++i) {
			long count = 0;
			if (nums[i] == 1)
				continue;
			for (int j = i + 1; j < n; ++j) {
				if (nums[j] < nums[i])
					count++;
			}
			midNums[(int) (n - nums[i])] = count;
		}
//		System.out.print("Mid Nums: ");
//		printArr(midNums);
		return midNums;
	}

	// 新中介数 -> 新排列数
	private static void mid2permutation(long[] nums, int n, long[] midNums) {
		for (int i = 0; i < n; ++i)
			nums[i] = 0L;

		for (int i = 0; i < n - 1; ++i) {
			int count = 0;
			for (int j = n - 1; j >= 0; --j) {
				if (count == midNums[i] && nums[j] == 0) {
					nums[j] = (long) (n - i);
					break;
				} else if (nums[j] == 0) {
					count++;
				}
			}
		}

		int idx = -1;
		while (nums[++idx] != 0)
			;

		nums[idx] = 1L;
	}

	// 逆置
	private static void reverse(long[] midNums, int start, int end) {
		long temp;
		for (int i = start, j = end; i < j; ++i, --j) {
			temp = midNums[i];
			midNums[i] = midNums[j];
			midNums[j] = temp;
		}
	}

	/**
	 * 字典序计算
	 * 
	 * @param nums:给定的序列
	 * @param n:序列的个数
	 * @param k:要此序列后第k个序列,k分为k>0和k<0
	 * 9 1 3 8 3 9 6 4 7 5 2 1
	 */
	private static void dictOrder(long[] nums, int n, long k) {
		// 原排列 -> 原中介数
		long[] midNums = new long[n - 1];
		for (int i = 0; i < n - 1; i++) {
			long count = 0;
			for (int j = i + 1; j < n; ++j) {
				if (nums[i] > nums[j])
					count++;
			}
			midNums[i] = count;
		}
		// 原中介数 -> 新中介数
		midNums[n - 1 - 1] += k;
		long temp = 0;
		long carry = 0;
		if (k > 0) {
			for (int i = n - 1 - 1; i >= 0; --i) {
				temp = midNums[i] + carry;
				midNums[i] = temp % (n - i);
				carry = temp / (n - i);
				if (carry == 0)
					break;
			}
		} else {
			// 下面的carry代表的是借位, 是正数
			// 最后一位<0
			if (midNums[n - 1 - 1] < 0) {
				for (int i = n - 1 - 1; i >= 0; --i) {
					temp = midNums[i] - carry;
					// 都归正数了,可以结束了
					if (temp >= 0) {
						midNums[i] = temp;
						break;
					}
					if (temp % (n - i) == 0) {
						midNums[i] = 0L;
						carry = -1 * temp / (n - i);
					} else {
						midNums[i] = n - i + temp % (n - i);
						carry = 1 - temp / (n - i);
					}
				}
			}
		}

		// 新中介数 -> 新排列
		boolean[] temps = new boolean[n];
		for (int i = 0; i < n; ++i)
			temps[i] = true;
		int subsum = 0;  // 现在nums0~(n-1)所存数,为了计算最后一个
		for (int i = 0; i < n - 1; ++i) {
			midNums[i] += 1;
			int count = 0;
			for (int j = 0; j < n; ++j) {
				if (temps[j])
					count++;
				if (count == midNums[i]) {
					temps[j] = false;
					nums[i] = j + 1;
					subsum += nums[i];
					break;
				}
			}
		}
		nums[n - 1] = n * (n + 1) / 2 - subsum;

	}

	/**
	 * 递增进位制计算
	 * 
	 * @param nums
	 * @param n
	 * @param k
	 */
	/*
	 * Input 9 2 1 8 3 9 6 4 7 5 2 1 Output 8 4 9 6 1 7 5 2 3
	 */
	private static void incrementalCarry(long[] nums, int n, long k) {
		// 原排列 -> 原中介数
		long[] midNums = permutation2Mid(nums, n);

		// 原中介数 -> 新中介数:加减k
		midNums[n - 1 - 1] += k; // 最后一位做加法
		long temp = 0L, carry = 0L;
		if (k > 0) {
			for (int i = n - 1 - 1; i >= 0; --i) {
				temp = midNums[i] + carry;
				carry = temp / (n - i);
				midNums[i] = temp % (n - i);
			}
		} else {
			// 下面的carry代表的是借位, 是正数
			// 最后一位<0
			if (midNums[n - 1 - 1] < 0) {
				for (int i = n - 1 - 1; i >= 0; --i) {
					temp = midNums[i] - carry;
					// 都归正数了,可以结束了
					if (temp >= 0) {
						midNums[i] = temp;
						break;
					}
					if (temp % (n - i) == 0) {
						midNums[i] = 0L;
						carry = -1 * temp / (n - i);
					} else {
						midNums[i] = n - i + temp % (n - i);
						carry = 1 - temp / (n - i);
					}
				}
			}
		}

		// 新中介数 -> 新序列数
		mid2permutation(nums, n, midNums);
	}

	/**
	 * 递减进位制计算
	 * 
	 * @param nums
	 * @param n
	 * @param k
	 */
	/*
	 * Input 9 3 1 8 3 9 6 4 7 5 2 1 Output 8 9 3 6 4 7 5 2 1
	 */
	private static void degressiveCarry(long[] nums, int n, long k) {
		// 原排列 -> 原中介数
		long[] midNums = permutation2Mid(nums, n);
		// 逆置得递减进位的中介数
		reverse(midNums, 0, midNums.length - 1);

		// 原中介数 -> 新中介数:加减k
		midNums[n - 1 - 1] += k; // 最后一位做加法
		long temp = 0L, carry = 0L;
		if (k > 0) {
			for (int i = n - 1 - 1; i >= 0; --i) {
				temp = midNums[i] + carry;
				carry = temp / (i + 2);
				midNums[i] = temp % (i + 2);
			}
		} else {
//			TODO
			// 下面的carry代表的是借位, 是正数
			// 最后一位<0
			if (midNums[n - 1 - 1] < 0) {
				for (int i = n - 1 - 1; i >= 0; --i) {
					temp = midNums[i] - carry;
					// 都归正数了,可以结束了
					if (temp >= 0) {
						midNums[i] = temp;
						break;
					}
					if (temp % (i + 2) == 0) {
						midNums[i] = 0L;
						carry = -1 * temp / (i + 2);
					} else {
						midNums[i] = i + 2 + temp % (i + 2);
						carry = 1 - temp / (i + 2);
					}
				}
			}
		}
		reverse(midNums, 0, midNums.length - 1);
//		System.out.print("Mid Nums ± K: ");
//		printArr(midNums);

		// 新中介数 -> 新排列
		mid2permutation(nums, n, midNums);
	}

	/**
	 * 邻位对换算法
	 * 
	 * @param nums
	 * @param n
	 * @param k
	 */
	/*
	 * Input 9 4 1 8 3 9 6 4 7 5 2 1 Output 8 3 6 9 4 7 5 2 1
	 */
	private static void orthoSubstitution(long[] nums, int n, long k) {
		// 原排列 -> 中介数
		long[] midNums = new long[n - 1];

		for (int i = 0; i < n - 1; ++i) {
			int j = 0;
			int count = 0;

			while (nums[j] != i + 2)
				// 先统计左边比i+2小的
				if (nums[j++] < i + 2)
					count++;

			// 如果 i+2 为 奇数 ,其方向性决定于 b(i-1) 的奇偶性, 奇向右、偶向左 。
			if ((i + 2) % 2 == 1) {
				// 只有偶数向左的时候,才需要重新计数,奇数已经计数过了
				if (midNums[i - 1] % 2 == 0) {
					count = 0;
					while (j < n)
						if (nums[j++] < i + 2)
							count++;
				}
			}
			// 2 一定向左,如果 i 为 偶数 ,其方向性决定于 b(i-1) + b(i-2) 的奇偶性,同样是 奇向右、偶向左 。
			else {
				// 只有偶数向左的时候,才需要重新计数,奇数已经计数过了
				if (i + 2 == 2 || (midNums[i - 1] + midNums[i - 2]) % 2 == 0) {
					count = 0;
					while (j < n)
						if (nums[j++] < i + 2)
							count++;
				}
			}
			midNums[i] = (long) count;
		}

//		System.out.print("Mid Nums: ");
//		printArr(midNums);

		// 原中介数 -> 新中介数
		// 原中介数 -> 新中介数:加减k
		midNums[n - 1 - 1] += k; // 最后一位做加法
		if (k > 0) {
			long temp = 0L, carry = 0L;
			for (int i = n - 1 - 1; i >= 0; --i) {
				temp = midNums[i] + carry;
				carry = temp / (i + 2);
				midNums[i] = temp % (i + 2);
			}
		} else {
//					TODO
			// 下面的carry代表的是借位, 是正数
			// 最后一位<0
			if (midNums[n - 1 - 1] < 0) {
				long temp = 0L, carry = 0L;
				for (int i = n - 1 - 1; i >= 0; --i) {
					temp = midNums[i] - carry;
					// 都归正数了,可以结束了
					if (temp >= 0) {
						midNums[i] = temp;
						break;
					}
					if (temp % (i + 2) == 0) {
						midNums[i] = 0L;
						carry = -1 * temp / (i + 2);
					} else {
						midNums[i] = i + 2 + temp % (i + 2);
						carry = 1 - temp / (i + 2);
					}
				}
			}
		}

		for (int i = 0; i < n; ++i)
			nums[i] = 0L;

		for (int i = n - 2; i >= 0; --i) {
			int j;
			int count = 0;
			// i+2为奇,
			if (i % 2 == 1) {
				// 只看b(i-1)
				if (midNums[i - 1] % 2 == 1)
					// 向右第b(i)+1个空
					for (j = 0; j < n; j++) {
						if (count == midNums[i] && nums[j] == 0)
							break;
						else if (nums[j] == 0)
							count++;
					}
				else
					// 向左
					for (j = n - 1; j >= 0; j--) {
						if (count == midNums[i] && nums[j] == 0)
							break;
						else if (nums[j] == 0)
							count++;

					}
			}
			// i 为 2,一定向左 // i+2 为偶数
			else {
				// 要看b(i-1) + b(i-2)
				if (i + 2 != 2 && (midNums[i - 1] + midNums[i - 2]) % 2 == 1)
					// 向右
					for (j = 0; j < n; j++) {
						if (count == midNums[i] && nums[j] == 0)
							break;
						else if (nums[j] == 0)
							count++;

					}
				else
					// 向左
					for (j = n - 1; j >= 0; j--) {
						if (count == midNums[i] && nums[j] == 0)
							break;
						else if (nums[j] == 0)
							count++;

					}
			}
			nums[j] = (long) (i + 2);
		}

		int idx = -1;
		while (nums[++idx] != 0)
			;

		nums[idx] = 1L;
	}

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章