HeapPermute算法正确性和时间效率的分析
算法课的习题要求分析HeapPermute算法的正确性,实在是费劲,目前已花费鄙人数小时之久。参阅了之前11级和12级学长/学姐的博客,个人认为证明是错误的,所以自己整理了一个版本(分歧点列在了算法正确性分析的后面)。因为算法正确性的证明实在太麻烦,所以部分步骤有省略,望海涵。
0. 习题
HeapPermute(n)
//实现生成排列的 Heap 算法
//输入:一个正整数 n 和一个全局数组 A[1…n](注意n是正整数,不是什么正正整数,题目近10年来一直有这个错误也是醉了)
//输出:A 中元素的全排列
if n = 1
write A
else
for i ← 1 to n do
HeapPermute(n − 1)
if n is odd
swap A[1] and A[n]
else
swap A[i] and A[n]注:全排列就是数组中的所有元素(各元素均不同)的排列顺序,如A=[1,2,3]时,其全排列就是[1,2,3], [1,3,2], [2,1,3], [2,3,1],[3,1,2], [3,2,1]. 该结果和顺序也是利用HeapPermute(3)算法处理数组A=[1,2,3]时的输出结果及其顺序。
为方便书写和阅读,下面的分析中将 “HeapPermute”简写为“HP”。
1. 算法正确性分析
设全局数组 A A A的长度为 n ( n 为正整数 ) n (n为正整数) n(n为正整数). 经过分析, 可以得到如下待证明结论:
设 k k k 为正整数且 k ≤ n k \le n k≤n. 在执行算法 HP( k k k)后, 输出 A A A 前 k k k 位的全排列和后 ( n − k ) (n-k) (n−k) 位的组合. 特别地, 当 k = n k = n k=n 时, 输出的就是 A A A 的全排列. 执行算法 HP( k k k) 前后, 当 k k k 为奇数时, A A A 保持不变;当 n n n 为偶数时, A A A 的前 k k k 位循环右移一位。
易见, 执行算法HP( n n n)可以输出全局数组 A = [ 1.. n ] A=[1 .. n] A=[1..n] 的全排列是结论①的一个特例. 因此只需证明结论①。
下面用数学归纳法进行分析证明结论①. 假设全局数组
A
=
[
a
1
,
a
2
,
⋯
,
a
n
]
A=[a_1, a_2, \cdots, a_n]
A=[a1,a2,⋯,an],
n
n
n 为正整数。
(1) 当
k
=
1
k = 1
k=1 时, 执行 HP(
k
k
k) 后, 输出的
[
a
1
,
a
2
,
⋯
,
a
n
]
[a_1, a_2, \cdots, a_n]
[a1,a2,⋯,an] 是
A
A
A 第
1
1
1 位的全排列和
A
A
A 后
(
n
−
1
)
(n-1)
(n−1) 位的组合,
A
=
[
a
1
,
a
2
,
⋯
,
a
n
]
A = [a_1, a_2, \cdots, a_n]
A=[a1,a2,⋯,an], 保持不变, 与结论相符;
(2) 当
k
=
2
k = 2
k=2 且
k
≤
n
k \le n
k≤n 时, 执行 HP(
k
k
k) 后, 输出的
[
a
1
,
a
2
,
a
3
,
⋯
,
a
n
]
[a_1, a_2, a_3, \cdots, a_n]
[a1,a2,a3,⋯,an] 和
[
a
2
,
a
1
,
a
3
,
⋯
,
a
n
]
[a_2, a_1, a_3, \cdots, a_n]
[a2,a1,a3,⋯,an], 是
A
A
A 前
2
2
2 位的全排列和
A
A
A 后
n
−
2
n-2
n−2 位的组合,
A
=
[
a
2
,
a
1
,
a
3
,
⋯
,
a
n
]
A = [a_2, a_1, a_3, \cdots, a_n]
A=[a2,a1,a3,⋯,an] 且
A
A
A 的前
k
k
k 位循环右移一位, 与结论相符;
(3) 假设
k
=
2
m
−
1
(
m
为正整数
)
k = 2m -1 (m 为正整数)
k=2m−1(m为正整数) 且
k
<
n
k < n
k<n 时, 执行 HP(
k
k
k) 后, 输出
A
A
A 前
k
k
k 位的全排列和后
(
n
−
k
)
(n-k)
(n−k) 位的组合, 且
A
A
A 保持不变. 当
k
=
2
m
k = 2m
k=2m 时, 算法直接进入第4行. 对于
i
=
1
,
2
,
⋯
,
2
m
i = 1,2,\cdots,2m
i=1,2,⋯,2m时, 分别输出此时
A
A
A 的前
(
2
m
−
1
)
(2m-1)
(2m−1) 位的全排列与
A
A
A 的后
(
n
−
2
m
+
1
)
(n-2m+1)
(n−2m+1) 位的组合, 进入第6行, 由于
n
n
n 为偶数, 将
A
A
A 的第
i
i
i 位与第
k
=
2
m
k=2m
k=2m 位互换. 上述
i
=
1
,
2
,
⋯
,
2
m
i=1, 2, \cdots, 2m
i=1,2,⋯,2m 的所有操作, 仅改变了原始
A
A
A 的前
k
=
2
m
k=2m
k=2m 位, 只是输出
A
A
A 的前
(
2
m
−
1
)
(2m-1)
(2m−1) 个元素全排列和
A
A
A 的后
(
n
−
2
m
+
1
)
(n-2m+1)
(n−2m+1) 位的组合, 然后依次将
A
A
A 的第
1
,
2
,
⋯
,
2
m
1,2,\cdots,2m
1,2,⋯,2m 位调换至 第
2
m
2m
2m 位. 易见, 上述操作, 得到了
A
A
A 的前
k
k
k 位的全排列和后
(
n
−
k
)
(n-k)
(n−k) 位的组合, 且
A
A
A 的前
k
k
k 位循环右移一位. 与结论相符;
(4) 假设
k
=
2
m
(
m
为正整数
)
k = 2m (m为正整数)
k=2m(m为正整数) 且
k
<
n
k < n
k<n 时, 执行 HP(
k
k
k) 后, 输出
A
A
A 前
k
k
k 位的全排列和后
(
n
−
k
)
(n-k)
(n−k) 位的组合, 且
A
A
A 的前
k
k
k 位循环右移一位. 类似(3)可以证明, 当
k
=
2
m
+
1
k = 2m+1
k=2m+1 时, 算法 HP(
k
k
k)的执行结果与结论相符。
因此,结论得证。
注:分歧点
个人认为之前几位学长/学姐的证明的错误之处在于假定的是HP( n n n)和数组A(长度为 n n n)的关系,而非HP( k k k)与数组A的关系。这里 n n n如题干所言是数组 A A A的长度, k k k是正整数且 k ≤ n k \le n k≤n. 如果假定的是HP( n n n)和数组 A A A的关系, 那么无法直接在原有逻辑上使用数学归纳法证明。原因在于证明的第二步有误。假设 HP( n − 1 n-1 n−1)对数组 A A A(长度为 ( n − 1 ) (n-1) (n−1))的操作满足结论,说明的是HP( n − 1 n-1 n−1)对长度为 ( n − 1 ) (n-1) (n−1)的数组有效,而证明HP( n n n)对数组A( n n n)的操作满足结论需要的前提是HP( n − 1 n-1 n−1)对长度为 n n n的数组有效(具体效果如本节开头的结论所述)。 欢迎各位的探讨~
2. 算法时间效率分析
分别考虑输出排列和交换两种操作的算法时间效率分析。
(1) 只考虑输出排列的耗时时, 由于算法本身输出的是长度为
n
n
n 的数组
A
A
A 的全排列, 因此算法复杂度
T
1
(
n
)
=
n
!
T_1(n)=n!
T1(n)=n!;
(2) 只考虑交换操作的耗时时, 可以得到算法复杂度的如下递推表达式
T
2
(
n
)
=
{
0
,
i
f
n
=
1
n
[
T
2
(
n
−
1
)
+
1
]
,
i
f
n
≥
2
T_2(n) = \begin{cases} 0, & {\rm if}~n =1\\ n \left[ T_2(n-1) + 1 \right], & {\rm if}~n \ge 2 \end{cases}
T2(n)={0,n[T2(n−1)+1],if n=1if n≥2
推导可以得到
T
2
(
n
)
=
∑
i
=
1
n
−
1
A
n
i
T_2(n)=\sum_{i=1}^{n-1} A_{n}^{i}
T2(n)=∑i=1n−1Ani。因此,这部分的计算复杂度可以记为
O
(
n
n
−
1
)
O(n^{n-1})
O(nn−1)。
由于当 n n n 为正整数时, n n − 1 ≥ n ! n^{n-1} \ge n! nn−1≥n! 始终成立。因此,由 O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x { f ( n ) , g ( n ) } ) , O(f(n)) + O(g(n)) = O({\rm max}\{ f(n), g(n)\}), O(f(n))+O(g(n))=O(max{f(n),g(n)}),算法复杂度为 T 2 ( n ) = O ( n n − 1 ) T_2(n) = O(n^{n-1}) T2(n)=O(nn−1)。
3. Python代码
(1) 简洁版代码
# -*- coding: utf-8 -*-
"""
Created on Thu Apr 9 21:27:14 2020
@author: AbaloneVH
"""
import numpy as np
def swap(x, y):
# 为了简便,可以省略swap函数,直接使用 x, y = y, x
return y, x
def HP(n, A):
if n == 1:
print(A)
return A
else:
for i in range(n):
A = HP(n-1, A)
if n%2 == 1:
A[0], A[n-1] = swap(A[0], A[n-1])
else:
A[i], A[n-1] = swap(A[i], A[n-1])
return A
N = 5 # 数列长度, 可调
A = np.arange(1,N+1)
A = HP(len(A), A)
(2) 测试用代码
# -*- coding: utf-8 -*-
"""
Created on Thu Apr 9 21:27:14 2020
@author: AbaloneVH
"""
import numpy as np
def swap(x, y):
return y, x
def HP(n, A, sum1, sum2):
# sum1: 输出排列的次数
# sum2: 交换的次数
if n == 1:
print(A)
sum1 += 1
return A, sum1, sum2
else:
for i in range(n):
A, sum1, sum2 = HP(n-1, A, sum1, sum2)
if n%2 == 1:
A[0], A[n-1] = swap(A[0], A[n-1])
sum2 += 1
else:
A[i], A[n-1] = swap(A[i], A[n-1])
sum2 += 1
return A, sum1, sum2
N = 5 # 数列长度, 可调
A = np.arange(1,N+1)
A, sum1, sum2 = HP(len(A), A, 0, 0)