組合數學--排列組合

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;
	}

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