算法设计与分析之递归与分类策略

引入

  任何用计算机求解的问题所需的计算时间都与其规模有关。当问题的规模较大时,处理问题的方式尤为重要。分治算法的思想便是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,即逐个击破,分而治之。

1 递归的概念

  递归算法:直接或间接调用自己的算法。

  例1.1:阶乘函数
(1.1) n ! = { 1 , x 0 n ( n 1 ) ! , x > 0 n!= \begin{cases} 1,\quad x\leq 0\\ n(n-1)!, \quad x>0 \end{cases} \tag{1.1}   代码如下:

程序清单1.1:

def test(n):
    if n == 0:
        return 1;
    else:
        return n * test1(n - 1)

if __name__ == '__main__':
    print("The answer is:", test(10))

  运行结果:

The answer is: 3628800

  例1.2:Fibonacci数列
(1.2) F ( n ) = { 1 , x 0 F ( n 1 ) + F ( n 2 ) , x > 0 F(n)= \begin{cases} 1,\quad x\leq 0\\ F(n-1)+F(n-2), \quad x>0 \end{cases} \tag{1.2}   代码如下:

程序清单1.2:

def test(n):
    if n == 0 or n == 1:
        return 1
    else:
        return test(n - 1) + test(n - 2)

if __name__ == '__main__':
    print("The answer is:", test(10))

  运行结果:

The answer is: 89

  以上两例中的函数也可以用以下非递归方式定义:
(1.3) n ! = { 1 2 3 ( n 1 ) n n!= \begin{cases} 1*2*3*···*(n-1)*n \end{cases} \tag{1.3}

(1.4) F ( n ) = 1 5 ( ( 1 + 5 2 ) n + 1 ( 1 5 2 ) n + 1 ) F(n)= \frac{1}{\sqrt{5}}((\frac{1+\sqrt{5}}{2})^{n+1}-(\frac{1-\sqrt{5}}{2})^{n+1}) \tag{1.4}   例1.3:Ackerman函数
  并非一切递归函数都能用非递归方式定义,例如如下的双递归函数:
(1.5) { A ( 1 , 0 ) = 2 A ( 0 , m ) = 1                           m 0 A ( n , 0 ) = n + 2                         n 2 A ( n , m ) = A ( A ( n 1 , m ) , m 1 )       n , m 1 \begin{cases} A(1,0)=2\\ A(0,m)=1              m\geq0\\ A(n,0)=n+2            n\geq2\\ A(n,m)=A(A(n-1,m),m-1)   n,m\geq1 \end{cases} \tag{1.5}   代码如下:

程序清单1.3:

def test(n, m):
    if n == 1 or m == 0:
        return 2
    elif n == 0:
        return 1
    elif m == 0:
        return n + 2
    else:
        return test(test(n - 1, m), m - 1)

if __name__ == '__main__':
    print("The answer is:", test(10, 9))

  运行结果:

The answer is: 2

  例1.4:排列问题
  设 R = { r 1 , r 2 , , r n } R=\{r_1,r_2,···,r_n\} 是要进行排列的元素, R i = R { r i } R_i=R-\{r_i\} 。集合 X X 中元素的全排列记为 p e r m ( X ) perm(X) ( r i ) p e r m ( X ) (r_i)perm(X) 表示在全排列 p e r m ( X ) perm(X) 的每一个排列前加上前缀 r i r_i 得到的排列。 R R 的全排列可归纳定义如下:
  当 n = 1 n=1 时, p e r m ( R ) = ( r ) perm(R)=(r) ,其中 r r 是集合 R R 中唯一的元素;
  当 n > 1 n>1 时, p e r m ( R ) = ( r ) perm(R)=(r) ( r 1 ) p e r m ( R ) = ( r 1 ) (r_1)perm(R)=(r_1) ( r 2 ) p e r m ( R ) = ( r 2 ) (r_2)perm(R)=(r_2) ,···, ( r n ) p e r m ( R ) = ( r n ) (r_n)perm(R)=(r_n) 构成。
  
  代码如下:

程序清单1.4:

def test(num_list, k, m):
    if k == m:
        print(num_list)
    else:
        for i in range(k, m + 1):
            swab(num_list, k, i)
            test(num_list, k + 1, m)
            swab(num_list, k, i)

def swab(num_list, k, m):    #交换
    temp_value = num_list[k]
    num_list[k] = num_list[m]
    num_list[m] = temp_value

if __name__ == '__main__':
    test([9, 5, 4, 3, 6, 7, 2], 0, 6)

  运行结果:

[9, 5, 4, 3, 6, 7, 2]
[9, 5, 4, 3, 6, 2, 7]
[9, 5, 4, 3, 7, 6, 2]
···
[2, 9, 5, 4, 7, 3, 6]
[2, 9, 5, 4, 3, 7, 6]
[2, 9, 5, 4, 3, 6, 7]

  简单说来,即是将第k号元素与第m号元素进行交换。
  
  例1.5:整数划分问题
  将正整数 n n 表示成一系列正整数之和, n = n 1 + n 2 + + n 3 n=n_1+n_2+···+n_3 ,其中 n 1 n 2 n k 1 n_1\ge n_2\ge ····\ge n_k\ge 1 k 1 k\ge 1
  正整数 n n 的这种表示称为正整数 n n 的划分。正整数 n n 的不同划分的个数称为正整数 n n 的划分数,记作 p ( n ) p(n)
  例如,正整数6有如下11中不同的划分,所以 p ( 6 ) = 11 p(6)=11
  6;
  5+1;
  4+2;4+1+1;
  3+3;3+2+1;3+1+1+1;
  2+2+2;2+2+1+1;2+1+1+1+1;
  1+1+1+1+1+1。
  在正整数 n n 的所以不同的划分中,将最大加数 n 1 m n_1\le m 的划分个数记作 q ( n , m ) q(n,m) 。于是建立如下递归关系:
  (1) q ( n , 1 ) = 1 q(n,1)=1 n 1 n\ge 1
  即:最大加数 n 1 1 n_1\le 1 时,任何正整数 n n 只有一种划分形式,即 n = 1 + 1 + + 1 n n=\overbrace{1+1+···+1}^n
  (2) q ( n , m ) = q ( n , n ) q(n,m)=q(n,n) n m n\le m
  即:最大加数 n 1 n_1 实际上不能大于1。因此 q ( 1 , m ) = 1 q(1,m)=1
  (3) q ( n , n ) = 1 + q ( n , n 1 ) q(n,n)=1+q(n,n-1)
  即:正整数 n n 的加分由 n 1 = n n_1=n 的划分和 n 1 n 1 n_1\le n-1 的划分组成。
  (4) q ( n , m ) = q ( n , m 1 ) + q ( n m , m ) q(n,m)=q(n,m-1)+q(n-m,m) n > m > 1 n>m>1
  即:正整数 n n 的最大加数 n 1 n_1 不大于 m m 的划分由 n 1 = m n_1=m 的划分和 n 1 m 1 n_1\le m-1 的划分组成。

  以上归纳为:
(1.6) q ( n , m ) = { 1                               n = 1 m = 1 q ( n , n )                           m 1 + q ( n , n 1 )                     n = m q ( n , m 1 ) + q ( n m , m )           n > m > 1 q(n,m)= \begin{cases} 1                n=1,m=1\\ q(n,n)              n\leq m\\ 1+q(n,n-1)          n=m\\ q(n,m-1)+q(n-m,m)      n>m>1 \end{cases} \tag{1.6}   其中,正整数 n n 的划分数为 p ( n ) = q ( n , n ) p(n)=q(n,n)

  代码如下:

程序清单1.5:

def test(n ,m):
    if n < 1 or m < 1:
        return 0
    if n == 1 or m == 1:
        return 1
    if n < m:
        return test(n ,n)
    if n == m:
        return test(n, m-1) + 1
    return test(n, m - 1) + test(n - m, m)

if __name__ == '__main__':
    print("The answer is:",test(6, 6))

  运行结果:

The answer is: 11

  例1.6:Hanio塔问题:
  设 A , B , C A,B,C 是3个塔座。开始时,在塔座 A A 上有一叠共 n n 个圆盘,这些圆盘自上而下,由大到小叠在一起。各圆盘由小到大编号为 1 , 2 , , n 1,2,···,n ,如下图(图片来源):
  在这里插入图片描述

图1-1 汉诺塔模型

  
  现要求将塔座 A A 上着一叠圆盘移到塔座 B B 上,并仍按同样顺序叠置。在移动圆盘时必须遵守以下规则:
  (1)每次只能移动1个圆盘;
  (2)任何时刻都不能将较大圆盘放在较小圆盘上;
  (3)三座塔均可使用。
  思路:
  假设塔座 A , B , C A,B,C 排成一个三角形, A B C A A\rightarrow B\rightarrow C\rightarrow A 构成一顺时针循环。在移动圆盘的过程中,若是奇数次移动,则将最小的圆盘移动到顺时针方向的下一个塔座上;若是偶数次移动,则保持最小的圆盘不动,而在其他两个塔座之间,将较小的圆盘移到另一个塔座上。

  代码如下:

程序清单1.6:

def test(n, a, b, c):
    if n > 0:
        test(n - 1, a, c, b)
        print(":", a, "->", b)
        test(n - 1, c, b, a)

if __name__ == '__main__':
    test(3, 'A', 'B', 'C')

  运行结果:

A -> B
A -> C
B -> C
A -> B
C -> A
C -> B
A -> B

  递归算法结果清晰,可读性强,且容易用数学归纳法加以证明。但是,其往往效率较低,无论是空间还是存储上。因此,有时希望在递归算法中消除递归调用,使其转化为非递归算法。通常,消除递归采用来模拟系统的递归调用,并根据具体程序的特点对递归调用工作栈进行简化,尽量减小栈操作,压缩栈存储空间等。

2 分治法基本思想

  分治法的基本思想:
  将一个规模为 n n 的问题分解为 k k 个规模较小的问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各子问题的解合并得到原问题的解。其伪代码如下:

求解(P):
	if:问题P规模小于n0
		求解问题P
	else:
		将问题P划分为k个子问题
		for i in k:
			子问题解 = 求解(Pi)
	返回 所有解进行合并

  根据分治法的分割原则,将原问题分为多少个子问题比较合适?每个子问题规模相当?大量实践发现,将一个问题分成大小相等的 k k 个子问题是行之有效的,且大多时候 k = 2 k=2

2.1 二分搜索

  二分搜索是分治法的典型例子,其思想很简单:即已排序数组左右双向搜索。

  代码如下:

程序清单2.1:

def binary_search(num_list, x):
    left = 0
    right = len(num_list)
    while left < right:
        middle = int((left + right) / 2)
        if x == num_list[middle]:
            return middle
        elif x > num_list[left]:
            left += 1
        else:
            right += 1
    return -1

if __name__ == '__main__':
    answer = binary_search([1,2,6,8,9,10,15], 10)
    print("The index is:", answer)

  运行结果:

The answer is: 5

2.2 棋盘覆盖

  在一个 2 k 2 k 2^k*2^k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。显然,特殊方格出现在棋盘上的位置有 4 k 4^k 种情形。因而对任何 k 0 k\ge 0 ,有 4 k 4^k 种不同的特殊棋盘。特别的 k = 2 k=2 时,有如下图(来源):
在这里插入图片描述

图2-1 k=2时的一个特殊棋盘


  在棋盘覆盖问题中,要用图2-2所示的四种不同形态的 L L 型骨牌覆盖给定的特殊棋盘上除特殊方格外的所有方格,且不同 L L 骨牌之间不允许交叠。易知,所用到的 L L 型骨牌个数恰为 ( 4 k 1 ) / 3 (4^k-1)/3 个。
在这里插入图片描述

图2-2 4种不同形态的骨牌


  用分治策略,可以设计出解棋盘覆盖问题的简介算法:
  当 k &gt; 0 k&gt;0 时,将 2 k 2 k 2^k*2^k 棋盘分个为4个 2 k 1 2 k 1 2^{k-1}*2^{k-1} 子棋盘,如下图2-3(a):
 在这里插入图片描述

图2-3 棋盘分割


  特殊方格必定位于4个较小棋盘之一中,其余三个字棋盘则无。为了将这3个无特殊格子的子棋盘转化为特殊棋盘,可以用一个 L L 型骨牌覆盖着三个子棋盘的会和处,如图2-3(b),从而将原问题简化为4个较小规模的棋盘覆盖问题。

  代码如下:

程序清单2.2:

待续...

2.3 合并排序

  合并排序基本思想:将待排序元素分成大致相同的两个子数组,再将排好序的子数组合并并排序。用递归可表示如下:

def merge_sort(a, left, right):   #输入参数:待排序数组、数组左右索引
	if left < right:	#至少两个元素
		middle= (left + right) / 2    #取中点
		merge_sort(a, left, middle)
		merge_sort(a, middle+ 1)
		merge(a, b, left, middle, right)    #合并到数组b中
		copy(a, b, left, right)    #复制回数组b

  算法merge_sort的递归过程只是将待排序数组一分为二,直到待排序数组只剩下1个元素位置。然后不断合并2个排好序的数组段。
  按此机制,可以先将数组a中相邻元素两两配对。用合并算法将它们排序,构成 n / n/ 2组长度为2的排好序的子数组段,然后再两两合并直至整个数组排好序。故消除递归后的合并排序算法如下:

def merge_sort(a):    #输入参数:数组a
	b = []
	s = 1
	while s < len(a):
		merge
原创文章 35 获赞 44 访问量 8644

猜你喜欢

转载自blog.csdn.net/weixin_44575152/article/details/100108398