HeapPermute算法正确性和时间效率的分析

算法课的习题要求分析HeapPermute算法的正确性,实在是费劲,目前已花费鄙人数小时之久。参阅了之前11级和12级学长/学姐的博客,个人认为证明是错误的,所以自己整理了一个版本(分歧点列在了算法正确性分析的后面)。因为算法正确性的证明实在太麻烦,所以部分步骤有省略,望海涵。

0. 习题

HeapPermute(n)
//实现生成排列的 Heap 算法
//输入:一个正整数 n 和一个全局数组 A[1…n](注意n是正整数,不是什么正正整数,题目近10年来一直有这个错误也是醉了)
//输出:A 中元素的全排列
if n = 1         (1)
 write A        (2)
else          (3)
 for i ← 1 to n do    (4)
  HeapPermute(n − 1) (5)
 if n is odd       (6)
  swap A[1] and A[n]  (7)
 else swap A[i] and A[n] (8)

注:全排列就是数组中的所有元素(各元素均不同)的排列顺序,如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 kn. 在执行算法 HP( k k k)后, 输出 A A A k k k 位的全排列和后 ( n − k ) (n-k) (nk) 位的组合. 特别地, 当 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) (n1) 位的组合, 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 kn 时, 执行 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 n2 位的组合, 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=2m1(m) k < n k < n k<n 时, 执行 HP( k k k) 后, 输出 A A A k k k 位的全排列和后 ( n − k ) (n-k) (nk) 位的组合, 且 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) (2m1) 位的全排列与 A A A 的后 ( n − 2 m + 1 ) (n-2m+1) (n2m+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) (2m1) 个元素全排列和 A A A 的后 ( n − 2 m + 1 ) (n-2m+1) (n2m+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) (nk) 位的组合, 且 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) (nk) 位的组合, 且 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 kn. 如果假定的是HP( n n n)和数组 A A A的关系, 那么无法直接在原有逻辑上使用数学归纳法证明。原因在于证明的第二步有误。假设 HP( n − 1 n-1 n1)对数组 A A A(长度为 ( n − 1 ) (n-1) (n1))的操作满足结论,说明的是HP( n − 1 n-1 n1)对长度为 ( n − 1 ) (n-1) (n1)的数组有效,而证明HP( n n n)对数组A( n n n)的操作满足结论需要的前提是HP( n − 1 n-1 n1)对长度为 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 ) , i f n ≥ 2 T_2(n) = \begin{cases} 0, & {\rm if} n =1\\ n + T_2(n-1), & {\rm if} n \ge 2 \end{cases} T2(n)={ 0,n+T2(n1),ifn=1ifn2
推导可以得到 T 2 ( n ) = n ! + O ( n n − 1 ) T_2(n)=n! + O(n^{n-1}) T2(n)=n!+O(nn1). 由于当 n n n 为正整数时, n ! ≥ n n − 1 n! \ge n^{n-1} n!nn1 始终成立。因此,由
O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x { f ( n ) , g ( n ) } ) , O(f(n)) + O(g(n)) = O(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(nn1)

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):
  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)

猜你喜欢

转载自blog.csdn.net/AbaloneVH/article/details/105443942