问题简介
本文将介绍计算机算法中的经典问题——最大子数组问题(maximum subarray problem)。所谓的最大子数组问题,指的是:给定一个数组A,寻找A的和最大的非空连续子数组。比如,数组 A = [-2, -3, 4, -1, -2, 1, 5, -3], 最大子数组应为[4, -1, -2, 1, 5],其和为7。
首先,如果A中的元素全部为正(或非负数),则最大子数组就是它本身;如果A中的元素全部为负,则最大子数组就是第一个元素组成的数组。以上两种情形是平凡的,那么,如果A中的元素既有正数,又有负数,则该如何求解呢?本文将介绍该问题的四种算法,并给出后面三种算法的Python语言实现,解决该问题的算法如下:
- 暴力求解
- 分治法
- Kadane算法
- 动态规划法
下面就这四种算法做详细介绍。
暴力求解
假设数组的长度为n,暴力求解方法的思路是很简单的,就是将子数组的开始坐标和结束坐标都遍历一下,这样共有$C_{n}^{2}$中组合方式,再考虑这所有组合方式中和最大的情形即可。
该算法的运行时间为$O(n^{2})$,效率是很低的。那么,还有其它高效的算法吗?
分治法
分治法的基本思想是将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小,递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解,最后将子问题的解组合成原问题的解。
对于最大子数组,我们要寻求子数组A[low…high]的最大子数组。令mid为该子数组的中央位置,我们考虑求解两个子数组A[low…mid]和A[mid+1…high]。A[low…high]的任何连续子数组A[i…j]所处的位置必然是以下三种情况之一:
- 完全位于子数组A[low…mid]中,因此$low\leq i\leq j \leq mid.$
- 完全位于子数组A[mid+1…high]中,因此$mid< i\leq j \leq high.$
- 跨越了中点,因此$low \leq i \leq mid < j \leq high.$
因此,最大子数组必定为上述3种情况中的最大者。对于情形1和情形2,可以递归地求解,剩下的就是寻找跨越中点的最大子数组。
任何跨越中点的子数组都是由两个子数组A[i…mid]和A[mid+1…j]组成,其中$low \leq i \leq mid$且$mid<j\leq high$.因此,我们只需要找出形如A[i…mid]和A[mid+1…j]的最大子数组,然后将其合并即可,这可以在线性时间内完成。过程FIND-MAX-CROSSING-SUBARRAY接收数组A和下标low、mid和high作为输入,返回一个下标元组划定跨越中点的最大子数组的边界,并返回最大子数组中值的和。其伪代码如下:
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high):
left-sum = -inf
sum = 0
for i = mid downto low
sum = sum + A[i]
if sum > left-sum
left-sum = sum
max-left = i
right-sum = -inf
sum = 0
for j = mid+1 to high
sum = sum + A[j]
if sum > right-sum
right-sum = sum
max-right = i
return (max-left, max-right, left-sum+right+sum)
有了FIND-MAX-CROSSING-SUBARRAY我们可以找到跨越中点的最大子数组,于是,我们也可以设计求解最大子数组问题的分治算法了,其伪代码如下:
FIND-MAXMIMUM-SUBARRAY(A, low, high):
if high = low
return (low, high, A[low])
else
mid = floor((low+high)/2)
(left-low, left-high, left-sum) = FIND-MAXMIMUM-SUBARRAY(A, low, mid)
(right-low, right-high, right-sum) = FIND-MAXMIMUM-SUBARRAY(A, mid+1, high)
(cross-low, cross-high, cross-sum) = FIND-MAXMIMUM-SUBARRAY(A, low, mid, high)
if left-sum >= right-sum >= cross-sum
return (left-low, left-high, left-sum)
else right-sum >= left-sum >= cross-sum
return (right-low, right-high, right-sum)
else
return (cross-low, cross-high, cross-sum)
显然这样的分治算法对于初学者来说,有点难度,但是熟能生巧, 多学多练也就不难了。该分治算法的运行时间为$O(n*logn).$
Kadane算法
Kadane算法的伪代码如下:
Initialize:
max_so_far = 0
max_ending_here = 0
Loop for each element of the array
(a) max_ending_here = max_ending_here + a[i]
(b) if(max_ending_here < 0)
max_ending_here = 0
(c) if(max_so_far < max_ending_here)
max_so_far = max_ending_here
return max_so_far
Kadane算法的简单想法就是寻找所有连续的正的子数组(max_ending_here就是用来干这事的),同时,记录所有这些连续的正的子数组中的和最大的连续数组。每一次我们得到一个正数,就将它与max_so_far比较,如果它的值比max_so_far大,则更新max_so_far的值。
动态规划法
用MS[i]表示最大子数组的结束下标为i的情形,则对于i-1,有:
$$MS[i] = max\{MS[i-1], A[i]\}.$$
这样就有了一个子结构,对于初始情形,$MS[1]=A[1].$遍历i, 就能得到MS这个数组,其最大者即可最大子数组的和。
总结
可以看到以上四种算法,每种都有各自的优缺点。对于暴力求解方法,想法最简单,但是算法效率不高。Kanade算法简单高效,但是不易想到。分治算法运行效率高,但其分支过程的设计较为麻烦。动态规划法想法巧妙,运行效率也高,但是没有普遍的适用性。
Python程序
下面将给出分治算法,Kanade算法和动态规划法来求解最大子数组问题的Python程序, 代码如下:
# -*- coding: utf-8 -*-
__author__ = 'Jclian'
import math
# find max crossing subarray in linear time
def find_max_crossing_subarray(A, low, mid, high):
max_left, max_right = -1, -1
# left part of the subarray
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 part of the subarray
right_sum = float("-Inf")
sum = 0
for j in range(mid + 1, high + 1):
sum += A[j]
if (sum > right_sum):
right_sum = sum
max_right = j
return max_left, max_right, left_sum + right_sum
# using divide and conquer to solve maximum subarray problem
# time complexity: n*logn
def find_maximum_subarray(A, low, high):
if (high == low):
return low, high, A[low]
else:
mid = math.floor((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
# Python program to find maximum contiguous subarray
# Kadane’s Algorithm
def maxSubArraySum(a, size):
max_so_far = float("-inf")
max_ending_here = 0
for i in range(size):
max_ending_here = max_ending_here + a[i]
if (max_so_far < max_ending_here):
max_so_far = max_ending_here
if max_ending_here < 0:
max_ending_here = 0
return max_so_far
# using dynamic programming to slove maximum subarray problem
def DP_maximum_subarray(arr):
t = len(arr)
MS = [0]*t
MS[0] = arr[0]
for i in range(1, t):
MS[i] = max(MS[i-1]+arr[i], arr[i])
return MS
def main():
# example of array A
A = [13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7]
# A = [-2, 2, -3, 4, -1, 2, 1, -5, 3]
# A = [0,-2, 3, 5, -1, 2]
# A = [-9, -2, -3, -5, -3]
# A = [1, 2, 3, 4, 5]
# A = [-2, -3, 4, -1, -2, 1, 5, -3]
print('using divide and conquer...')
print("Maximum contiguous sum is",find_maximum_subarray(A, 0, len(A) - 1), '\n')
print('using Kanade Algorithm...')
print("Maximum contiguous sum is", maxSubArraySum(A, len(A)), '\n')
print('using dynamic programming...')
MS = DP_maximum_subarray(A)
print("Maximum contiguous sum is", max(MS), '\n')
main()
输出结果如下:
using divide and conquer...
Maximum contiguous sum is (7, 10, 43)
using Kanade Algorithm...
Maximum contiguous sum is 43
using dynamic programming...
Maximum contiguous sum is 43
参考文献
- 算法导论(第三版) 机械工业出版社
- https://www.geeksforgeeks.org…
- https://algorithms.tutorialho…
注意:本人现已开通两个微信公众号: 用Python做数学(微信号为:python_math)以及轻松学会Python爬虫(微信号为:easy_web_scrape), 欢迎大家关注哦~~