引入
任何用计算机求解的问题所需的计算时间都与其规模有关。当问题的规模较大时,处理问题的方式尤为重要。分治算法的思想便是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,即逐个击破,分而治之。
1 递归的概念
递归算法:直接或间接调用自己的算法。
例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:
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:Ackerman函数
并非一切递归函数都能用非递归方式定义,例如如下的双递归函数:
代码如下:
程序清单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:排列问题
设
是要进行排列的元素,
。集合
中元素的全排列记为
。
表示在全排列
的每一个排列前加上前缀
得到的排列。
的全排列可归纳定义如下:
当
时,
,其中
是集合
中唯一的元素;
当
时,
由
,
,···,
构成。
代码如下:
程序清单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:整数划分问题
将正整数
表示成一系列正整数之和,
,其中
,
。
正整数
的这种表示称为正整数
的划分。正整数
的不同划分的个数称为正整数
的划分数,记作
。
例如,正整数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。
在正整数
的所以不同的划分中,将最大加数
的划分个数记作
。于是建立如下递归关系:
(1)
,
。
即:最大加数
时,任何正整数
只有一种划分形式,即
。
(2)
,
。
即:最大加数
实际上不能大于1。因此
。
(3)
。
即:正整数
的加分由
的划分和
的划分组成。
(4)
,
。
即:正整数
的最大加数
不大于
的划分由
的划分和
的划分组成。
以上归纳为:
其中,正整数
的划分数为
。
代码如下:
程序清单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塔问题:
设
是3个塔座。开始时,在塔座
上有一叠共
个圆盘,这些圆盘自上而下,由大到小叠在一起。各圆盘由小到大编号为
,如下图(图片来源):
现要求将塔座
上着一叠圆盘移到塔座
上,并仍按同样顺序叠置。在移动圆盘时必须遵守以下规则:
(1)每次只能移动1个圆盘;
(2)任何时刻都不能将较大圆盘放在较小圆盘上;
(3)三座塔均可使用。
思路:
假设塔座
排成一个三角形,
构成一顺时针循环。在移动圆盘的过程中,若是奇数次移动,则将最小的圆盘移动到顺时针方向的下一个塔座上;若是偶数次移动,则保持最小的圆盘不动,而在其他两个塔座之间,将较小的圆盘移到另一个塔座上。
代码如下:
程序清单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 分治法基本思想
分治法的基本思想:
将一个规模为
的问题分解为
个规模较小的问题,这些子问题互相独立且与原问题相同。递归地解这些子问题,然后将各子问题的解合并得到原问题的解。其伪代码如下:
求解(P):
if:问题P规模小于n0
求解问题P
else:
将问题P划分为k个子问题
for i in k:
子问题解 = 求解(Pi)
返回 所有解进行合并
根据分治法的分割原则,将原问题分为多少个子问题比较合适?每个子问题规模相当?大量实践发现,将一个问题分成大小相等的 个子问题是行之有效的,且大多时候 。
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-2所示的四种不同形态的
型骨牌覆盖给定的特殊棋盘上除特殊方格外的所有方格,且不同
骨牌之间不允许交叠。易知,所用到的
型骨牌个数恰为
个。
用分治策略,可以设计出解棋盘覆盖问题的简介算法:
当
时,将
棋盘分个为4个
子棋盘,如下图2-3(a):
特殊方格必定位于4个较小棋盘之一中,其余三个字棋盘则无。为了将这3个无特殊格子的子棋盘转化为特殊棋盘,可以用一个
型骨牌覆盖着三个子棋盘的会和处,如图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中相邻元素两两配对。用合并算法将它们排序,构成
2组长度为2的排好序的子数组段,然后再两两合并直至整个数组排好序。故消除递归后的合并排序算法如下:
def merge_sort(a): #输入参数:数组a
b = []
s = 1
while s < len(a):
merge