組合數學--排列組合
1. 概述
組合數學這是筆者在研究生階段唯一的一門數學課了吧,希望做個了斷。
組合數學可以理解成是離散數學中的一部分,廣義的組合數學就是離散數學
離散數學可以理解成是狹義的組合數學和圖論、代數結構、數理邏輯的統稱
以上所說僅僅是叫法上的不同,總而言之組合數學是研究離散對象的科學,但是在計算機科學中有着重要的作用
1.1 應用
這裏只提幾個很出名的問題
- 幻方問題
- 四色問題
- 最短網絡
- 最小生成樹和最小斯坦納樹
- 無尺度網絡
- 小世界網絡
1.2 三大問題
- 存在(Existence Problem)
- 計數(Counting Problem)
- 優化(Optimization Problem)
2. 排列組合
最基本的排列組合無需多言
計數問題最重要的是做到無重複無遺漏,即不重不漏
2.1 兩大法則
- 加法法則:分類問題
- 乘法法則:分步問題
2.2 排列
- 圓排列
- 項鍊排列
- 多重全排列
- t種球,個數有限
- 打標號
- 多重全排列
- 分類枚舉
- 可重排列
3. 放球模型
- 排列:n個不同的球中取r個,放入r個不同的盒子,每個盒子一個
- 組合:n個不同的球中取r個,放入r個相同的盒子,每個盒子一個
4. 模型轉換
A事件不好計算,但是A和B一一對應,B好計算事件,可以轉換爲求B的,也就求出來了A的
如人打乒乓球賽,每個選手至少打一局,輸者淘汰,最後產生一名冠軍,需要比賽多少場?
答:場,因爲要選出來冠軍,淘汰的選手和比賽一一對應
Cayley定理:n個有標號的頂點的樹的數目是,用條邊將連接起來的連通圖的數目是
5. 線性方程的解
線性方程的非負整數解的個數是
【解釋】相當於把一堆x分成b組,每組個數不限,
5.1 若干等式及其組合意義
- 從走到的方法數
從取整數,對取法分類
- ,有種方案
- ,有種方案-
- 6 和 7 都是
- Vandemonde 恆等式
6. 全排列生成算法
這算是本章的重點了吧
全排列的個數是。現在的問題是
- 生成所有的排列,
- 根據某一個排列,計算之後或者之前第個排列是什麼。
注意一點,因爲是全排列,所以其含義包含着所有元素都不相同。
6.1 字典序法
經典的方法,就是按照從小到大枚舉變化即可。
舉例來說,,
可以假設初始有一個向左的箭頭,代表移動的方向,但是是否可以移動取決於在的方向上是否一個比小的數字存在。
字典序法想要的是這一個和下一個具有儘可能長的共同前綴,也即變化在儘可能短的後綴上。
在實際上理解的時候,從小到大的排列,就是把當前排列從右往左掃描,找到第一個下降的數字,並且把該數字和其後數字中比它大的最小的那一個交換,並把新的後續數字從小到大排列。例如:,←是上升的,←是下降的,所以就是要交換的那一個數字,把它和中較小的交換,並把從小到大排列,下一個就是。
6.1.1 序號
全排列的序號就是先於此排列的個數。
排列 | 123 | 132 | 213 | 231 | 312 | 321 |
---|---|---|---|---|---|---|
序號 | 0 | 1 | 2 | 3 | 4 | 5 |
6.1.2 康拓展開
百度定義:,其中,爲整數,並且。
6.1.3 中介數
字典序的中介數代表的是當前數字右邊比其小的數字的個數。計算當前排列後第個排列只要把當前中介數加上,然後再把新的中介數還原成排列數即是要求的排列數。
【注意】中介數和是不同的進制,要按照中介數的進制進行計算。
舉例:,它的序號也即康拓展開式,其中就是中介數,代表的是在當前數字比其右邊大的數字的個數。
由推出:
中介數,序號和排列之間是一一對應的關係。
可用歸納法證明:
6.2 遞增進位制
遞增進位制是不固定進制中的基數,從右向左數數,第個位置的數字逢進一位,即從右向左,逢進一位。
它的中介數是在的右邊比小的數字的個數。但是它的中介數的進制和字典序的一致,都是從右向左,逢進一位。
6.3 遞減進位制
遞減進位制和遞增進位制類似,它的中介數是把遞增進位制逆置,但是它的中介數的進位是從左往右,逢進一位。
6.4 SJT鄰位對換
它的方向是雙向的,通過保存數字的“方向性“來快速得到下一個排列。
設定爲我們要求的中介數。
- 規定的方向一定向左。就是從開始,背向的方向所有比小的數字的個數。
- 對於每一個比大的數字:
- 若爲奇數,其方向性決定於的奇偶性,奇向右,偶向左。
- 若爲偶數,其方向性決定於的奇偶性,奇向右,偶向左。
的值就是背向的方向直到排列邊界這個區間裏比小的數字的個數。
SJT方法的中介數進位同遞減進位制。
6.5 總結
其實還有很多方法,老師說可以參考的神書《計算機程序設計的藝術》裏面Permutation Generating。
上述方法是爲了讓新生成的排列和原排列的儘可能相似,就是換的數字儘可能少。
7. 代碼實現
並沒有很好的代碼格式,只是爲了應付OJ,又不會使用C++,所以湊合着使用了Java。其實本次OJ中C++的long long
夠用。
7.1 題目描述
給定一個到的排列,請求出這個排列根據某種順序的後面第個排列。
輸入格式
- 第一行是三個由空格隔開的整數,,;
- 第二行是個由空格隔開的中的無重複整數,表示一個排列;行末可能會有空格。
的含義如下:
- 當時,請按字典序計算;
- 當時,請按遞增進位制計算;
- 當時,請按遞減進位制計算;
- 當時,請按鄰位對換法的順序計算。
當時,請計算根據順序的前面第−k個排列。
輸出格式
第一行輸出個由單個空格隔開的整數,表示答案排列。
樣例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
數據規模
的取值保證答案存在。
存在以下幾種限制:
- 和;
- 和;
- 。
對於每一種限制組合,都有一個測試點,共個測試點。
時空限制
時間限制: 內存限制:
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;
}
}