第4章 分治策略
在分治策略中,我们递归地求解一个问题,在每层递归中运用如下三个步骤
- 分解(Divide):将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小
- 解决(Comquer):递归的求解出子问题,如果子问题的规模足够小,则停止递归,直接求解
- 合并(Combine):将子问题的解组合成原问题的解
递归情况和基本情况
- 当子问题足够大时,需要地柜求解,我们称之为递归情况(recursive case)
- 当子问题变得足够小,不再需要递归时,我们说递归已经”触底”,进入基本情况(base case)
递归式
- 一个递归式(recurrence)就是一个等式或不等式,它通过更小的输入上的函数值来描述一个函数
- 例如
- 三种求解递归式的方法
- 代入法:我们猜测一个界,然后用数学归纳法证明其正确性
- 递归树法:将递归式转换为一棵树,其节点表示不同层次的递归调用的代价,然后采用边界和技术求解
- 主方法:可求解形如下式的递归式
4.1 最大子数组问题
4.1.1 问题描述
寻找一个数组的和最大的非空连续子数组,这样的数组我们称为最大子数组(maximum subarray)
注意:只有当数组中包含负数时,最大子数组问题才有意义。如果所有数组元素都是非负的,最大子数组问题没有任何难度,因为整个数组的和肯定是最大的
4.1.2 暴力求解方法
简单的尝试每对可能的{数组下标,数组上标},寻找最大值
时间复杂度:O(n2),一共有(n-1)*n/2种情况
样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
def find_maximum_subarray(A): low = 0 high = 0 max_val = float('-inf') for i in range(0, len(A)): for j in range(i, len(A)): sum = 0 for s in range(i, j + 1): sum += A[s] if sum > max_val: max_val = sum low = i high = j return low, high, max_val if __name__ == '__main__': A = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7] print(find_maximum_subarray(A)) ''' (7, 10, 43) '''
4.1.3 使用分治策略的求解方法
首先我们把数组
A[low..high]
划分为两个规模尽量相等的子数组:A[low..mid]
和A[mid+1..high]
A[low..high]
的任何连续子数组A[i..j]
所处的位置必然是一下三种情况之一,如下图- 完全位于子数组
A[low..mid]
中,因此low<=i<=j<=mid
- 完全位于子数组
A[mid+1..high]
中,因此mid<i<=j<=high
- 跨越了中点,因此
low<=i<=mid<j<=high
- 完全位于子数组
因此
A[low..high]
的一个最大子数组所处的位置必定是这三种情况之一.我们可以递归的求解A[low..mid]
和A[mid+1..high]
的最大子数组,因为这两个子问题仍然是最大子数组问题,只是规模更小.因此,剩下的问题就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者我们可以很容易地使用线性时间(O(n))内求出跨越中点的最大子数组,算法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
def find_max_crossing_subarray(A, low, mid, high): """ 求解跨越了中点的最大子数组 :param A: 数组 :param low: 数组下标 :param mid: 数组中点 :param high: 数组上标 :return: """ print("find_max_crossing_subarray:", A, low, mid, high) # 这里需要考虑一种特殊情况,可以忽略但不影响逻辑理解 if low == mid: return find_max_crossing_subarray_base_case(A, low) left_sum = float("-inf") sum = 0 for i in range(mid, low - 1, -1): sum += A[i] if sum > left_sum: left_sum = sum max_left = i right_sum = float("-inf") sum = 0 for i in range(mid + 1, high + 1): sum += A[i] if sum > right_sum: right_sum = sum max_right = i return max_left, max_right, left_sum + right_sum
有了一个线性的
find_max_crossing_subarray
在手,我们就可以设计求解最大子数组问题的分治算法了1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
def find_maximum_subarray(A, low, high): """ 通过分治方法递归的求解最大子数组 :param A: 数组 :param low: 数组下标 :param high: 数组上标 :return: """ print("find_maximum_subarray:", A, low, high) if low == high: return low, high, A[low] else: mid = int((low + high) / 2) (left_low, left_high, left_sum) = find_maximum_subarray(A, low, mid) (right_low, right_high, right_sum) = find_maximum_subarray(A, mid + 1, high) (cross_low, cross_high, cross_sum) = find_max_crossing_subarray(A, low, mid, high) if left_sum >= right_sum and left_sum >= cross_sum: return left_low, left_high, left_sum elif right_sum >= left_sum and right_sum >= cross_sum: return right_low, right_high, right_sum else: return cross_low, cross_high, cross_sum
4.1.4 分治算法的分析
接下来我们建立一个递归式来分析分治算法
最终可以求得时间复杂度为:O(nlgn)