组合数学--排列组合
1. 概述
组合数学这是笔者在研究生阶段唯一的一门数学课了吧,希望做个了断。
组合数学可以理解成是离散数学中的一部分,广义的组合数学就是离散数学
离散数学可以理解成是狭义的组合数学和图论、代数结构、数理逻辑的统称
以上所说仅仅是叫法上的不同,总而言之组合数学是研究离散对象的科学,但是在计算机科学中有着重要的作用
1.1 应用
这里只提几个很出名的问题
- 幻方问题
- 四色问题
- 最短网络
- 最小生成树和最小斯坦纳树
- 无尺度网络
- 小世界网络
1.2 三大问题
- 存在(Existence Problem)
- 计数(Counting Problem)
- 优化(Optimization Problem)
2. 排列组合
最基本的排列组合无需多言
计数问题最重要的是做到无重复无遗漏,即不重不漏
2.1 两大法则
- 加法法则:分类问题
- 乘法法则:分步问题
2.2 排列
- 圆排列
P ( n , r ) r , 2 ≤ r ≤ n \frac {P(n, r)} r, 2 \leq r \leq n rP(n,r),2≤r≤n - 项链排列
P ( n , r ) r , 3 ≤ r ≤ n \frac {P(n,r)} r, 3 \leq r \leq n rP(n,r),3≤r≤n - 多重全排列
- t种球,个数有限
- 打标号
- 多重全排列
- 分类枚举
- 可重排列
n r n^r nr
3. 放球模型
- 排列:n个不同的球中取r个,放入r个不同的盒子,每个盒子一个
P ( n , r ) = n ∗ ( n − 1 ) ∗ . . . ∗ ( n − r + 1 ) = n ! ( n − r ) ! P(n, r) = n*(n-1)*...*(n-r+1)=\frac {n!} {(n-r)!} P(n,r)=n∗(n−1)∗...∗(n−r+1)=(n−r)!n! - 组合:n个不同的球中取r个,放入r个相同的盒子,每个盒子一个
C ( n , r ) = n ! r ! ( n − r ) ! C(n, r) = \frac {n!} {r!(n-r)!} C(n,r)=r!(n−r)!n!
4. 模型转换
A事件不好计算,但是A和B一一对应,B好计算事件,可以转换为求B的,也就求出来了A的
如
100
100
100人打乒乓球赛,每个选手至少打一局,输者淘汰,最后产生一名冠军,需要比赛多少场?
答:
99
99
99场,因为要选出来冠军,淘汰的选手和比赛一一对应
Cayley定理:n个有标号的顶点的树的数目是
n
n
−
2
n^{n-2}
nn−2,用
n
−
1
n-1
n−1条边将
1
,
2
,
.
.
.
,
n
1,2,...,n
1,2,...,n连接起来的连通图的数目是
n
n
−
2
n^{n-2}
nn−2
5. 线性方程的解
线性方程
x
1
+
x
2
+
.
.
.
+
x
n
=
b
x_1+x_2+...+x_n = b
x1+x2+...+xn=b的非负整数解的个数是
C
(
n
+
b
−
1
,
b
)
C(n+b-1, b)
C(n+b−1,b)
【解释】相当于把一堆x分成b组,每组个数不限,
5.1 若干等式及其组合意义
- 从 ( 0 , 0 ) (0,0) (0,0)走到 ( m , n ) (m,n) (m,n)的方法数 C ( m + n , m ) C(m+n, m) C(m+n,m)
- C ( n , r ) = C ( n , n − r ) C(n, r) = C(n, n-r) C(n,r)=C(n,n−r)
-
C
(
n
,
r
)
=
C
(
n
−
1
,
r
)
+
C
(
n
−
1
,
r
−
1
)
C(n, r) = C(n-1, r) + C(n-1, r-1)
C(n,r)=C(n−1,r)+C(n−1,r−1)
从 1 ≤ a 1 ≤ a 2 ≤ . . . ≤ a n ≤ n 1\leq a_1 \leq a_2 \leq ... \leq a_n \leq n 1≤a1≤a2≤...≤an≤n取整数,对取法分类
- a 1 = 1 a_1 = 1 a1=1,有 C ( n − 1 , r − 1 ) C(n-1, r-1) C(n−1,r−1)种方案
- a 1 > 1 a_1 \gt 1 a1>1,有 C ( n − 1 , r ) C(n-1, r) C(n−1,r)种方案 - C ( n + r + 1 , r ) = C ( n + r , r ) + C ( n + r − 1 , r − 1 ) + . . . + 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) C(n+r+1,r)=C(n+r,r)+C(n+r−1,r−1)+...+C(n+1,1)+C(n,0)
- C ( n , l ) C ( l , r ) = C ( n , r ) C ( n − r , l − r ) C(n,l) C(l, r) = C(n, r)C(n-r, l-r) C(n,l)C(l,r)=C(n,r)C(n−r,l−r)
- C ( m , 0 ) + C ( m , 1 ) + . . . + C ( m , m ) = 2 m , m ≥ 0 C(m, 0) + C(m, 1) +...+C(m, m) = 2^m, m \geq 0 C(m,0)+C(m,1)+...+C(m,m)=2m,m≥0
-
C
(
n
,
0
)
−
C
(
n
,
1
)
+
C
(
n
,
2
)
−
.
.
.
±
C
(
n
,
n
)
=
0
C(n,0) - C(n, 1)+C(n,2)-...\pm C(n,n) = 0
C(n,0)−C(n,1)+C(n,2)−...±C(n,n)=0
- 6 和 7 都是 ( x + y ) n (x+y)^n (x+y)n
- Vandemonde 恒等式 C ( m + n , r ) = C ( m , 0 ) C ( n , r ) + C ( m , 1 ) C ( n , r − 1 ) + . . . + 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) 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!
n!。现在的问题是
- 生成所有的排列,
- 根据某一个排列,计算之后或者之前第 k k k个排列是什么。
注意一点,因为是全排列,所以其含义包含着所有元素都不相同。
6.1 字典序法
经典的方法,就是按照从小到大枚举变化即可。
举例来说,
1234
1234
1234,
1234
,
1243
,
1324
,
1342
,
1423
,
1432
2134
,
2143
,
2314
,
2341
,
2413
,
2431
3124
,
3142
,
3214
,
3241
,
3412
,
3421
4123
,
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\\
1234,1243,1324,1342,1423,14322134,2143,2314,2341,2413,24313124,3142,3214,3241,3412,34214123,4132,4213,4231,4312,4321
可以假设初始有一个向左的箭头,代表
k
k
k移动的方向,但是
k
k
k是否可以移动取决于在
k
k
k的方向上是否一个比
k
k
k小的数字存在。
字典序法想要的是这一个和下一个具有尽可能长的共同前缀,也即变化在尽可能短的后缀上。
在实际上理解的时候,从小到大的排列,就是把当前排列从右往左扫描,找到第一个下降的数字,并且把该数字和其后数字中比它大的最小的那一个交换,并把新的后续数字从小到大排列。例如:
132
132
132,
3
3
3←
2
2
2是上升的,
1
1
1←
3
3
3是下降的,所以
1
1
1就是要交换的那一个数字,把它和
23
23
23中较小的
2
2
2交换,并把
13
13
13从小到大排列,
132
132
132下一个就是
213
213
213。
6.1.1 序号
全排列的序号就是先于此排列的个数。
排列 | 123 | 132 | 213 | 231 | 312 | 321 |
---|---|---|---|---|---|---|
序号 | 0 | 1 | 2 | 3 | 4 | 5 |
6.1.2 康拓展开
百度定义: X = a [ n ] ∗ ( n − 1 ) ! + a [ n − 1 ] ∗ ( n − 2 ) ! + . . . + a [ i ] ∗ ( i − 1 ) ! + . . . + a [ 1 ] ∗ 0 ! X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! X=a[n]∗(n−1)!+a[n−1]∗(n−2)!+...+a[i]∗(i−1)!+...+a[1]∗0!,其中, a [ i ] a[i] a[i]为整数,并且 0 ≤ a [ i ] < i , ( 1 ≤ i ≤ n ) 0\leq a[i]\lt i, (1 \leq i \leq n) 0≤a[i]<i,(1≤i≤n)。
6.1.3 中介数
字典序的中介数代表的是当前数字右边比其小的数字的个数。计算当前排列后第
k
k
k个排列只要把当前中介数加上
k
k
k,然后再把新的中介数还原成排列数即是要求的排列数。
【注意】中介数和
k
k
k是不同的进制,要按照中介数的进制进行计算。
举例:
839647521
839647521
839647521,它的序号也即康拓展开式
7
∗
8
!
+
2
∗
7
!
+
6
∗
6
!
+
4
∗
5
!
+
2
∗
4
!
+
3
∗
3
!
+
2
∗
2
!
+
1
∗
1
!
7*8!+2*7!+6*6!+4*5!+2*4!+3*3!+2*2!+1*1!
7∗8!+2∗7!+6∗6!+4∗5!+2∗4!+3∗3!+2∗2!+1∗1!,其中
72642321
72642321
72642321就是中介数,
72642321
72642321
72642321代表的是在当前数字比其右边大的数字的个数。
由
72642321
72642321
72642321推出
839647521
839647521
839647521:
P
1
P
2
P
3
P
4
P
5
P
6
P
7
P
8
P
9
P_1 P_2 P_3 P_4 P_5 P_6 P_7 P_8 P_9
P1P2P3P4P5P6P7P8P9
- 7 + 1 = 8 → P 1 = 8 7+1=8→P_1 = 8 7+1=8→P1=8
- 2 + 1 = 3 → P 2 = 3 2+1=3→P_2=3 2+1=3→P2=3
- 6 + 1 = 7 , 但 3 < 7 已 在 P 3 左 边 出 现 , 7 + 1 = 8 , 但 8 已 在 P 3 左 边 出 现 , 8 + 1 = 9 → P 3 = 9 6+1=7,但3<7已在P_3左边出现,7+1=8,但8已在P_3左边出现,8+1=9→P_3=9 6+1=7,但3<7已在P3左边出现,7+1=8,但8已在P3左边出现,8+1=9→P3=9
- 4 + 1 = 5 , 但 3 < 5 已 在 P 4 左 边 出 现 , 5 + 1 = 6 → P 4 = 6 4+1=5,但3<5已在P_4左边出现,5+1=6→P_4=6 4+1=5,但3<5已在P4左边出现,5+1=6→P4=6
- 2 + 1 = 3 , 但 3 已 在 P 5 左 边 出 现 , 3 + 1 = 4 → P 5 = 4 2+1=3,但3已在P_5左边出现,3+1=4→P_5=4 2+1=3,但3已在P5左边出现,3+1=4→P5=4
- 3 + 1 = 4 , 但 3 , 4 已 在 P 6 左 边 出 现 , 4 + 1 + 1 = 6 , 但 6 已 在 P 6 左 边 出 现 , 6 + 1 = 7 → P 6 = 7 3+1=4,但3,4已在P_6左边出现,4+1+1=6,但6已在P_6左边出现,6+1=7→P_6=7 3+1=4,但3,4已在P6左边出现,4+1+1=6,但6已在P6左边出现,6+1=7→P6=7
- 2 + 1 = 3 , 但 3 已 在 P 7 左 边 出 现 , 3 + 1 = 4 , 但 4 已 在 P 7 左 边 出 现 , 4 + 1 = 5 → P 7 = 5 2+1=3,但3已在P_7左边出现,3+1=4,但4已在P_7左边出现,4+1=5→P_7=5 2+1=3,但3已在P7左边出现,3+1=4,但4已在P7左边出现,4+1=5→P7=5
- 1 + 1 = 2 → P 8 = 2 1+1=2→P_8=2 1+1=2→P8=2
- P 9 = 1 P_9 = 1 P9=1
中介数,序号和排列之间是一一对应的关系。
可用归纳法证明:
∑
k
=
1
n
−
1
k
∗
k
!
=
n
!
−
1
\sum_{k=1}^{n-1}{k*k!} = n! - 1
k=1∑n−1k∗k!=n!−1
6.2 递增进位制
递增进位制是不固定进制中的基数,从右向左数数,第
i
i
i个位置的数字逢
i
+
1
i+1
i+1进一位,即从右向左,逢
2
、
3
、
4
、
5
、
.
.
.
2、3、4、5、...
2、3、4、5、...进一位。
它的中介数是在
i
i
i的右边比
i
i
i小的数字的个数。但是它的中介数的进制和字典序的一致,都是从右向左,逢
2
、
3
、
4
、
5
、
.
.
.
2、3、4、5、...
2、3、4、5、...进一位。
6.3 递减进位制
递减进位制和递增进位制类似,它的中介数是把递增进位制逆置,但是它的中介数的进位是从左往右,逢 2 、 3 、 4 、 5 、 . . . 2、3、4、5、... 2、3、4、5、...进一位。
6.4 SJT邻位对换
它的方向是双向的,通过保存数字的“方向性“来快速得到下一个排列。
设定
b
2
,
b
3
,
b
4
,
b
5
,
b
6
,
b
7
,
b
8
,
b
9
b_2, b_3, b_4, b_5, b_6, b_7, b_8, b_9
b2,b3,b4,b5,b6,b7,b8,b9为我们要求的中介数。
- 规定 2 2 2的方向一定向左。 b 2 b_2 b2就是从 2 2 2开始,背向 2 2 2的方向所有比 2 2 2小的数字的个数。
- 对于每一个比
2
2
2大的数字
i
i
i:
- 若 i i i为奇数,其方向性决定于 b i − 1 b_{i-1} bi−1的奇偶性,奇向右,偶向左。
- 若 i i i为偶数,其方向性决定于 b i − 1 + b i − 2 b_{i-1}+b_{i-2} bi−1+bi−2的奇偶性,奇向右,偶向左。
b
i
b_i
bi的值就是背向
i
i
i的方向直到排列边界这个区间里比
i
i
i小的数字的个数。
SJT方法的中介数进位同递减进位制。
6.5 总结
其实还有很多方法,老师说可以参考
D
o
n
a
l
d
E
r
v
i
n
K
n
u
t
h
Donald Ervin Knuth
DonaldErvinKnuth的神书《计算机程序设计的艺术》里面Permutation Generating。
上述方法是为了让新生成的排列和原排列的尽可能相似,就是换的数字尽可能少。
7. 代码实现
并没有很好的代码格式,只是为了应付OJ,又不会使用C++,所以凑合着使用了Java。其实本次OJ中C++的long long
够用。
7.1 题目描述
给定一个
1
1
1到
n
n
n的排列
P
P
P,请求出这个排列根据某种顺序的后面第
k
k
k个排列。
输入格式
- 第一行是三个由空格隔开的整数 n n n, t y p e type type, k k k;
- 第二行是 n n n个由空格隔开的 [ 1 , . . . , n ] [1, ..., n] [1,...,n]中的无重复整数,表示一个排列;行末可能会有空格。
t y p e type type的含义如下:
- 当 t y p e = 1 type = 1 type=1时,请按字典序计算;
- 当 t y p e = 2 type=2 type=2时,请按递增进位制计算;
- 当 t y p e = 3 type=3 type=3时,请按递减进位制计算;
- 当 t y p e = 4 type=4 type=4时,请按邻位对换法的顺序计算。
当 k < 0 k<0 k<0时,请计算根据顺序的前面第−k个排列。
输出格式
第一行输出
n
n
n个由单个空格隔开的整数,表示答案排列。
样例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
数据规模
k
k
k的取值保证答案存在。
存在以下几种限制:
- 1 ≤ n ≤ 10 1≤n≤10 1≤n≤10和 1 ≤ n ≤ 20 1≤n≤20 1≤n≤20;
- k > 0 k>0 k>0和 k < 0 k<0 k<0;
- t y p e = 1 , 2 , 3 , 4 type=1, 2, 3, 4 type=1,2,3,4。
对于每一种限制组合,都有一个测试点,共 2 × 2 × 4 = 16 2×2×4=16 2×2×4=16个测试点。
时空限制
时间限制:
1000
m
s
1000ms
1000ms 内存限制:
512
M
i
B
512MiB
512MiB
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;
}
}