组合数学--排列组合

  • ​​1. 概述​​
  • ​​1.1 应用​​
  • ​​1.2 三大问题​​
  • ​​2. 排列组合​​
  • ​​2.1 两大法则​​
  • ​​2.2 排列​​
  • ​​3. 放球模型​​
  • ​​4. 模型转换​​
  • ​​5. 线性方程的解​​
  • ​​5.1 若干等式及其组合意义​​
  • ​​6. 全排列生成算法​​
  • ​​6.1 字典序法​​
  • ​​6.1.1 序号​​
  • ​​6.1.2 康拓展开​​
  • ​​6.1.3 中介数​​
  • ​​6.2 递增进位制​​
  • ​​6.3 递减进位制​​
  • ​​6.4 SJT邻位对换​​
  • ​​6.5 总结​​
  • ​​7. 代码实现​​
  • ​​7.1 题目描述​​
  • ​​7.2 代码实现​​

1. 概述

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

1.1 应用

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

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

1.2 三大问题

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

2. 排列组合

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

2.1 两大法则

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

2.2 排列

  • 圆排列
    组合数学--排列组合_组合数学
  • 项链排列
    组合数学--排列组合_排列组合_02
  • 多重全排列
  • t种球,个数有限
  • 打标号
  • 多重全排列
  • 分类枚举
  • 可重排列
    组合数学--排列组合_组合数学_03

3. 放球模型

  • 排列:n个不同的球中取r个,放入r个不同的盒子,每个盒子一个
    组合数学--排列组合_组合数学_04
  • 组合:n个不同的球中取r个,放入r个相同的盒子,每个盒子一个
    组合数学--排列组合_排列组合_05

4. 模型转换

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

组合数学--排列组合_组合数学_06人打乒乓球赛,每个选手至少打一局,输者淘汰,最后产生一名冠军,需要比赛多少场?
答:组合数学--排列组合_排列组合_07场,因为要选出来冠军,淘汰的选手和比赛一一对应
Cayley定理:n个有标号的顶点的树的数目是组合数学--排列组合_离散数学_08,用组合数学--排列组合_排列组合_09条边将组合数学--排列组合_组合数学_10连接起来的连通图的数目是组合数学--排列组合_离散数学_08

5. 线性方程的解

线性方程组合数学--排列组合_组合数学_12的非负整数解的个数是组合数学--排列组合_离散数学_13
【解释】相当于把一堆x分成b组,每组个数不限,

5.1 若干等式及其组合意义

  1. 组合数学--排列组合_组合数学_14走到组合数学--排列组合_排列组合_15的方法数组合数学--排列组合_组合数学_16
  2. 组合数学--排列组合_离散数学_17
  3. 组合数学--排列组合_离散数学_18
    组合数学--排列组合_离散数学_19取整数,对取法分类
    - 组合数学--排列组合_排列组合_20,有组合数学--排列组合_排列组合_21种方案
    - 组合数学--排列组合_排列组合_22,有组合数学--排列组合_离散数学_23种方案
  4. 组合数学--排列组合_离散数学_24
  5. 组合数学--排列组合_排列组合_25
  6. 组合数学--排列组合_排列组合_26
  7. 组合数学--排列组合_排列组合_27
  • 6 和 7 都是组合数学--排列组合_排列组合_28
  1. Vandemonde 恒等式组合数学--排列组合_离散数学_29

6. 全排列生成算法

这算是本章的重点了吧
全排列的个数是组合数学--排列组合_排列组合_30。现在的问题是

  1. 生成所有的排列,
  2. 根据某一个排列,计算之后或者之前第组合数学--排列组合_组合数学_31个排列是什么。

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

6.1 字典序法

经典的方法,就是按照从小到大枚举变化即可。
举例来说,组合数学--排列组合_组合数学_32
组合数学--排列组合_组合数学_33
可以假设初始有一个向左的箭头,代表组合数学--排列组合_离散数学_34移动的方向,但是组合数学--排列组合_离散数学_34是否可以移动取决于在组合数学--排列组合_离散数学_34的方向上是否一个比组合数学--排列组合_离散数学_34小的数字存在。
字典序法想要的是这一个和下一个具有尽可能共同前缀,也即变化在尽可能后缀上。
在实际上理解的时候,从小到大的排列,就是把当前排列从右往左扫描,找到第一个下降的数字,并且把该数字和其后数字中比它大的最小的那一个交换,并把新的后续数字从小到大排列。例如:组合数学--排列组合_排列组合_38组合数学--排列组合_排列组合_39组合数学--排列组合_组合数学_40是上升的,组合数学--排列组合_排列组合_41组合数学--排列组合_排列组合_39是下降的,所以组合数学--排列组合_排列组合_41就是要交换的那一个数字,把它和组合数学--排列组合_排列组合_44中较小的组合数学--排列组合_组合数学_40交换,并把组合数学--排列组合_组合数学_46从小到大排列,组合数学--排列组合_排列组合_38下一个就是组合数学--排列组合_排列组合_48

6.1.1 序号

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

排列

123

132

213

231

312

321

序号

0

1

2

3

4

5

6.1.2 康拓展开

百度定义组合数学--排列组合_排列组合_49,其中,组合数学--排列组合_组合数学_50为整数,并且组合数学--排列组合_组合数学_51

6.1.3 中介数

字典序的中介数代表的是当前数字右边比其小的数字的个数。计算当前排列后第组合数学--排列组合_离散数学_34个排列只要把当前中介数加上组合数学--排列组合_离散数学_34,然后再把新的中介数还原成排列数即是要求的排列数。
【注意】中介数和组合数学--排列组合_离散数学_34是不同的进制,要按照中介数的进制进行计算。
举例:组合数学--排列组合_组合数学_55,它的序号也即康拓展开式组合数学--排列组合_组合数学_56,其中组合数学--排列组合_离散数学_57就是中介数,组合数学--排列组合_离散数学_57代表的是在当前数字比其右边大的数字的个数。
组合数学--排列组合_离散数学_57推出组合数学--排列组合_组合数学_55
组合数学--排列组合_离散数学_61

  1. 组合数学--排列组合_离散数学_62
  2. 组合数学--排列组合_组合数学_63
  3. 组合数学--排列组合_组合数学_64
  4. 组合数学--排列组合_排列组合_65
  5. 组合数学--排列组合_离散数学_66
  6. 组合数学--排列组合_离散数学_67
  7. 组合数学--排列组合_离散数学_68
  8. 组合数学--排列组合_排列组合_69
  9. 组合数学--排列组合_离散数学_70

中介数,序号和排列之间是一一对应的关系。

组合数学--排列组合_组合数学_71


可用归纳法证明:组合数学--排列组合_离散数学_72

6.2 递增进位制

递增进位制是不固定进制中的基数,从右向左数数,第组合数学--排列组合_离散数学_73个位置的数字逢组合数学--排列组合_排列组合_74进一位,即从右向左,逢组合数学--排列组合_组合数学_75进一位。
它的中介数是在组合数学--排列组合_组合数学_76的右边比组合数学--排列组合_组合数学_76小的数字的个数。但是它的中介数的进制和字典序的一致,都是从右向左,逢组合数学--排列组合_组合数学_75进一位。

6.3 递减进位制

递减进位制和递增进位制类似,它的中介数是把递增进位制逆置,但是它的中介数的进位是从左往右,逢组合数学--排列组合_组合数学_75进一位。

6.4 SJT邻位对换

它的方向是双向的,通过保存数字的“方向性“来快速得到下一个排列。
设定组合数学--排列组合_组合数学_80为我们要求的中介数。

  • 规定组合数学--排列组合_离散数学_81的方向一定向左。组合数学--排列组合_离散数学_82就是从组合数学--排列组合_离散数学_81开始,背向组合数学--排列组合_离散数学_81的方向所有比组合数学--排列组合_离散数学_81小的数字的个数。
  • 对于每一个比组合数学--排列组合_离散数学_81大的数字组合数学--排列组合_组合数学_76:
  • 组合数学--排列组合_组合数学_88奇数,其方向性决定于组合数学--排列组合_排列组合_89的奇偶性,奇向右,偶向左
  • 组合数学--排列组合_组合数学_88偶数,其方向性决定于组合数学--排列组合_排列组合_91的奇偶性,奇向右,偶向左

组合数学--排列组合_离散数学_92的值就是背向组合数学--排列组合_离散数学_73的方向直到排列边界这个区间里比组合数学--排列组合_离散数学_73小的数字的个数。
SJT方法的中介数进位同递减进位制。

6.5 总结

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

7. 代码实现

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

7.1 题目描述

给定一个组合数学--排列组合_排列组合_41组合数学--排列组合_组合数学_97的排列组合数学--排列组合_排列组合_98,请求出这个排列根据某种顺序的后面第组合数学--排列组合_离散数学_34个排列。
输入格式

  • 第一行是三个由空格隔开的整数组合数学--排列组合_组合数学_100,组合数学--排列组合_组合数学_101,组合数学--排列组合_组合数学_31
  • 第二行是组合数学--排列组合_组合数学_100个由空格隔开的组合数学--排列组合_离散数学_104中的无重复整数,表示一个排列;行末可能会有空格。

组合数学--排列组合_离散数学_105的含义如下:

  • 组合数学--排列组合_离散数学_106时,请按字典序计算;
  • 组合数学--排列组合_组合数学_107时,请按递增进位制计算;
  • 组合数学--排列组合_排列组合_108时,请按递减进位制计算;
  • 组合数学--排列组合_排列组合_109时,请按邻位对换法的顺序计算。

组合数学--排列组合_组合数学_110时,请计算根据顺序的前面第−k个排列。

输出格式
第一行输出组合数学--排列组合_组合数学_97个由单个空格隔开的整数,表示答案排列。
样例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​数据规模
组合数学--排列组合_离散数学_34的取值保证答案存在。

存在以下几种限制:

  • 组合数学--排列组合_离散数学_113组合数学--排列组合_排列组合_114
  • 组合数学--排列组合_离散数学_115组合数学--排列组合_组合数学_116
  • 组合数学--排列组合_离散数学_117

对于每一种限制组合,都有一个测试点,共组合数学--排列组合_组合数学_118个测试点。

时空限制
时间限制:组合数学--排列组合_排列组合_119 内存限制:组合数学--排列组合_排列组合_120

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

}