全排列算法
Table of Contents
A permutation is an act of arranging the objects or numbers in order. Combinations are the way of selecting the objects or numbers from a group of objects or collection, in such a way that the order of the objects does not matter.
排列组合是组合/离散数学中的基础和重要的概念。
$$ P_n^m = A_n^m = n(n-1)(n-2)…(n-m+1) = \frac {n!}{(n-m)!} $$
当 \(n = m\) 时,也就是全排列:
\(P_n = n(n-1)(n-2)…3 \times 2 \times 1 = n!\)
默认是不放回排列,如果是重复排列,那么排列数就是 \(n^m\)
哈哈,跑偏了,这篇不是要演示 KaTex 的用法 (虽然确实挺有意思的 😹)
应用 #
排列算法 #
我们知道排列数是阶乘级增长,10个数的排列就达到了 \(10! = 3628800\),百万级的规模。
所以,即使我们 O(1)
生成一个排列,那生成全排列的算法复杂度也是 O(n!)
,非常恐怖。
在 wiki1 上有一个有意思的图,表示了九种算法,生成全排列的顺序。
Ordering of all permutations of length \({\displaystyle n=4}\) generated by different algorithms. The permutations are color-coded, where 1[❤️], 2[💛], 3[💚], 4[💙]
这九种算法分别是:
- 字典序
- 邻位对换法
- Heap 算法
- Ehrlich’s star-transposition algorithm
- Zaks’ prefix reversal algorithm
- Sawada-Williams’ algorithm
- Corbett’s algorithm
- Single-track ordering
- Single-track Gray code
下面是参考了一些网站2,用 Swift 实现常见的全排列生成算法。
回溯法 #
主要思路:交换、排列、恢复
func permute1(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
func _p(_ a: [Int], _ l: Int, _ r: Int) {
var tmp = a
if l == r {
ans.append(a)
return
}
for i in l...r {
tmp.swapAt(l, i)
_p(tmp, l + 1, r)
tmp.swapAt(i, l)
}
}
_p(nums, 0, nums.count - 1)
TOCK()
return ans
}
插入法 #
主要思路:对于 n 的全排列,将第 n+1 个数,插入到 (0…n) 这 n+1 个位置就可以得到 n+1 的全排列。
func permute2(_ nums: [Int]) -> [[Int]] {
TICK()
let ans : [[Int]] = nums.reduce([[]], { partialResult, a in
var nextResult = [[Int]]()
for lastArray in partialResult {
let sz = lastArray.count
// 将 a 插入到 n-1 排列中的 sz 个位置
for i in (0...sz) {
// ArraySlice<Int> 类型
let temp = lastArray[0..<i] + [a] + lastArray[i..<sz]
nextResult.append(Array(temp))
}
}
return nextResult
})
TOCK()
return ans
}
reduce
是个挺有意思的方法,有两个参数,一个是初始值,一个是闭包,闭包接收两个参数,一是上一步的结果,另一个是序列中的值。
上面的代码等价于:
var ans : [[Int]] = [[]]
for i in 0..<nums.count {
var nextResult = [[Int]]()
for (_, item) in ans.enumerated() {
let sz = item.count
// 将 a 插入到 n-1 排列中的 sz 个位置
for k in (0...sz) {
// ArraySlice<Int> 类型
let temp = item[0..<k] + [nums[i]] + item[k..<sz]
nextResult.append(Array(temp))
}
}
ans = nextResult
}
字典序 #
首次定义全升序为最小的序列,全降序为最大的序列。
问题变成从最小逐一生成下一个排列,生成下一个排列(nextPermutation) 的算法有四步:
- 从后向前找到第一个逆序的位置 \(j\), 即 \(p_j < p_{j+1} >= p_{j+2}\)
- 重新从后向前找到第一个比 \(p_j\) 大的数 \(p_k\),由于 \([j+1, n)\) 是降序的 (第一步决定), \(k\) 必然也是满足这个条件的最小的数,即 \(P_k = \min\{P_i | P_i > P_j, i > j\}\)
- 交换 \(j\) 和 \(k\) 位置的数
- 将 \([j+1, n)\) 反转 (降序变升序,其实就是重排序)
func permute3(_ nums: [Int]) -> [[Int]] {
TICK()
let nextPermutation : ([Int]) -> [Int]? = {
array in
var result = array
let sz = array.count
var j = -1
// 找到第一个比后面数小的数
for i in (0..<sz-1).reversed() {
if result[i] < result[i+1] {
j = i;
break;
}
}
// 找不到说明已经是全逆序,即最后一个排列
if (j == -1) {
return nil
}
var k = -1
// 找到 [j+1, sz) 中比 j 大的数中最小的一个 k
// 第一个就是最小的,因为 [j+1, sz) 是递减的
for i in (j+1..<sz).reversed() {
if result[i] > result[j] {
k = i
break
}
}
// 交换 j - k
result.swapAt(j, k)
// [j+1, sz)
result = Array(result[0...j] + result[j+1..<sz].reversed())
return result
}
var ans = [[Int]]()
ans.append(nums.sorted())
while let next = nextPermutation(ans.last!) {
ans.append(next)
}
TOCK()
return ans
}
递归法 ✨ #
主要思路是,枚举原数组所有位置,将位置上的数移到结果数组中,直到原数组清空。
func permute4(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
func _p(_ a: [Int], _ result: [Int]) {
let sz = a.count
if sz == 0 {
ans.append(result)
return
}
for i in 0..<sz {
let rest = Array(a[0..<i] + a[i+1..<sz])
_p(rest, result + [a[i]])
}
}
_p(nums, [])
TOCK()
return ans
}
邻位对换法3 #
也就是 Steinhaus–Johnson–Trotter
算法。
首先,定义升序是正方向,而对于 \(a[i] > a[j], i < j\),就是一个逆序对。
- 初始化一个方向数组,默认全 0,表示向左交换,如果变成 1,就是向右交换
- 确认一个元素是否能交换(活动状态):在它的方向上,它的邻位比自己小。
算法步骤就是:
- 初始化方向数组,全部向左交换
- 找到所有处于活动状态元素中最大的一个
- 将它与邻位交换
- 将所有大于上面这个元素(非活动状态)的其他元素方向反转
- 跳转 2,直至找不到这样的元素
func permute5(_ nums: [Int]) -> [[Int]] {
TICK()
var a = nums
let sz = a.count
var ans = [[Int]]()
// -1 means left, 1 means right
var directions = [Int](repeating: -1, count: sz)
func _movable() -> Int? {
var max = 1
var pos = -1
for i in 0..<sz {
if a[i] < max {
continue
}
if (directions[i] > 0 && i < sz - 1 && a[i] > a[i+1]) ||
(directions[i] < 0 && i > 0 && a[i] > a[i-1]) {
max = a[i]
pos = i
}
}
if (pos >= 0) {
return pos
}
return nil
}
ans.append(a)
while let max_i = _movable() {
let max_v = a[max_i]
a.swapAt(max_i, max_i + directions[max_i])
// 注意:方向也要跟着交换
directions.swapAt(max_i, max_i + directions[max_i])
ans.append(a)
for i in 0..<sz {
if (a[i] > max_v) {
directions[i] = -directions[i]
}
}
}
TOCK()
return ans
}
相比字典序来说,邻位交换在交换这一步更快,但是查找最大可移动元素和修改方向时,需要遍历。整体来说,时间差不多。
Heap 算法4 #
func _permute6(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
func _heap(k: Int, a: inout [Int]) {
if k == 1 {
ans.append(a)
return
}
// Generate permutations with k-th unaltered
// Initially k = length(A)
_heap(k: k-1, a: &a)
// Generate permutations for k-th swapped with each k-1 initial
for i in 0..<k-1 {
// Swap choice dependent on parity of k (even or odd)
if k % 2 == 0 {
a.swapAt(i, k-1)
} else {
a.swapAt(0, k-1)
}
_heap(k: k-1, a: &a)
}
}
var a = nums
_heap(k: a.count, a: &a)
TOCK()
return ans
}
递增进位法 #
func permute7(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
let sz = nums.count
var inc = [Int](repeating: 0, count: sz)
// inc[i] 表示 (i+1) 右边比它的小的数的个数,是 (i+1) 进制数
// inc[0] 总是 0
func _outInc() -> String {
let rev : [String] = inc[1...].reversed().map({ String($0) })
return rev.joined()
}
// 最低位是 2 进制,递增时进位多
func _increace() -> Bool {
var i = 1
var carry = 1
while i < sz {
if carry == 0 {
break
}
let sum = inc[i] + carry
inc[i] = sum % (i + 1)
carry = sum / (i + 1)
i += 1
}
// 如果还有进位,说明已经到了最大的排列
return (carry == 0)
}
func _next() -> Bool {
// 根据当前中介数求排列
var p = [Int](repeating: 0, count: sz)
for i in (0..<sz).reversed() {
var count = 0
var j = sz - 1
// inc[i] + 1 的空位,放 sz - i
while j >= 0 {
if p[j] == 0 {
count += 1
}
if (count > inc[i]) {
break
}
j -= 1
}
// p[j] = i + 1
p[j] = nums[i]
}
ans.append(p)
//print(_outInc())
// 中介数 + 1
return _increace()
}
while _next() {
}
TOCK()
return ans
}
递减进位法 #
func permute8(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
let sz = nums.count
var inc = [Int](repeating: 0, count: sz)
// inc[i] 表示 (n-i) 右边比它的小的数的个数,是 (n-i) 进制数
// inc[sz-1] 总是 0
func _outInc() -> String {
let rev : [String] = inc[0..<sz-1].reversed().map({ String($0) })
return rev.joined()
}
// 最低位是 n 进制,递增时进位少
func _increace() -> Bool {
var i = 0
var carry = 1
while i < sz - 1 {
if carry == 0 {
break
}
let sum = inc[i] + carry
inc[i] = sum % (sz - i)
carry = sum / (sz - i)
i += 1
}
// 如果还有进位,说明已经到了最大的排列
return (carry == 0)
}
func _next() -> Bool {
// 根据当前中介数求排列
var p = [Int](repeating: 0, count: sz)
for i in 0..<sz {
var count = 0
var j = sz - 1
// inc[i] + 1 的空位,放 sz - i
while j >= 0 {
if p[j] == 0 {
count += 1
}
if (count > inc[i]) {
break
}
j -= 1
}
// p[j] = sz - i
p[j] = nums[sz - i - 1]
}
ans.append(p)
//print(_outInc())
// 中介数 + 1
return _increace()
}
while _next() {
}
TOCK()
return ans
}
可以看出来,递减进位制和递增进位制法非常类似。区别仅在于:
- 中介数的最低位,递减法是 \(n\) 进制数,而递增法是 \(2\) 进制,这样在迭代时,递增法需要进位的次数更多。
快速排列 #
QuickPerm
算法5。
func permute9(_ nums: [Int]) -> [[Int]] {
TICK()
var ans = [[Int]]()
var a = nums
// init: [0,0,0...0]
var p = [Int](repeating: 0, count: a.count)
ans.append(a)
var i = 1
var j : Int
while i < a.count {
if (p[i] < i) {
// if i is odd, j = p[i]; else 0
j = i % 2 * p[i]
// swap(a[i], a[j])
(a[i], a[j]) = (a[j], a[i])
ans.append(a)
p[i] += 1
i = 1
} else {
p[i] = 0
i += 1
}
}
TOCK()
return ans
}
开始我没太理解这个算法的逻辑,后来看到了 Heap
算法的
非递归实现。
不能说比较相似吧,只能说一模一样。唯一的区别是这个算法改进了起始点从 i = 0
变成了 i = 1
,因为 i = 0
进入循环后什么也没有变化就进入了 i = 1
的状态。但是这一点优化,在数据量巨大的情况下,是质的提升!
另外,在它的官网上,这一实现被称为 Counting
,还有另一种实现叫
Countdown
,区别就在于 \(P[]\) 数组的初始化和修改方式,一个递增、一个递减。
耗时统计 ⌛ #
以下是通过 TICK/TOCK
宏生成的各个算法的执行时间 (以 10 个数排列为例)
算法 | 时间 |
---|---|
回溯法 | 4.97 s |
插入法 | 4.58 s |
字典序 | 8.24 s |
递归法 | 11.49 s |
邻位对换法 | 28.37 s |
Heap 算法 | 2.57 s |
递增进位法 | 16.70 s |
递减对换法 | 19.67 s |
快速排列算法 | 0.78 s |
不愧是 快速排列
算法!
- 统计宏
var g_start_time = NSDate()
public func TICK() {
g_start_time = NSDate()
}
public func TOCK(function: String = #function) {
print(String(format: "[%@] cost: %.2lf s", function, -g_start_time.timeIntervalSinceNow))
}
最佳实践 🚀 #
上面讲了非常多的算法,但是有些难以理解,我觉得对于同类算法,只需记住最容易理解的逻辑即可。
func permute(_ nums: [Int]) -> [[Int]] {
guard !nums.isEmpty else { return [] }
var ans = [[Int]]()
var a = nums
let n = nums.count
func _p(_ index: Int) {
if index == n {
ans.append(a)
return
}
for i in index..<n {
a.swapAt(index, i)
_p(index + 1)
a.swapAt(i, index)
}
}
_p(0)
return ans
}
关于去重 #
对于有重复元素的排列6:
最容易想到的是,不管重复元素,先求出所有排列。然后再去重。
我们在上面的最佳实践上稍加改动:
func permuteUnique(_ nums: [Int]) -> [[Int]] {
guard !nums.isEmpty else { return [] }
var ans = Set<[Int]>()
var a = nums
let n = nums.count
func _p(_ index: Int) {
if index == n {
ans.insert(a)
return
}
for i in index..<n {
a.swapAt(index, i)
_p(index + 1)
a.swapAt(i, index)
}
}
_p(0)
return Array(ans)
}
也还行,就是依赖 Set
的性质,有点耗时。那么怎么在求排列过程中直接忽略重复项呢?
func permuteUnique2(_ nums: [Int]) -> [[Int]] {
guard !nums.isEmpty else { return [] }
var ans = [[Int]]()
var a = nums
let n = nums.count
func _p(_ index: Int) {
if index == n {
ans.append(a)
return
}
var used = Set<Int>()
for i in index..<n {
if used.contains(a[i]) {
continue
}
used.insert(a[i])
a.swapAt(index, i)
_p(index + 1)
a.swapAt(index, i)
}
}
_p(0)
return ans
}
对于重复的元素只选择一次!
Cheers! 🎉