组合数学--排列组合
- 3. 放球模型
- 4. 模型转换
- 5. 线性方程的解
- 6.1.1 序号
- 6.1.2 康拓展开
- 6.1.3 中介数
- 6.2 递增进位制
- 6.3 递减进位制
- 6.4 SJT邻位对换
- 6.5 总结
1. 概述
组合数学这是笔者在研究生阶段唯一的一门数学课了吧,希望做个了断。
组合数学可以理解成是离散数学中的一部分,广义的组合数学就是离散数学
离散数学可以理解成是狭义的组合数学和图论、代数结构、数理逻辑的统称
以上所说仅仅是叫法上的不同,总而言之组合数学是研究离散对象的科学,但是在计算机科学中有着重要的作用
1.1 应用
这里只提几个很出名的问题
- 幻方问题
- 四色问题
- 最短网络
- 最小生成树和最小斯坦纳树
- 无尺度网络
- 小世界网络
1.2 三大问题
- 存在(Existence Problem)
- 计数(Counting Problem)
- 优化(Optimization Problem)
2. 排列组合
最基本的排列组合无需多言
计数问题最重要的是做到无重复无遗漏,即不重不漏
2.1 两大法则
2.2 排列
- 圆排列
- 项链排列
- 多重全排列
- 可重排列
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;
}
}