動態規劃法(八)最大子陣列問題(maximum subarray problem)
問題簡介
本文將介紹計算機演算法中的經典問題——最大子陣列問題(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,暴力求解方法的思路是很簡單的,就是將子陣列的開始座標和結束座標都遍歷一下,這樣共有中組合方式,再考慮這所有組合方式中和最大的情形即可。
該演算法的執行時間為,效率是很低的。那麼,還有其它高效的演算法嗎?
分治法
分治法的基本思想是將問題劃分為一些子問題,子問題的形式與原問題一樣,只是規模更小,遞迴地求解出子問題,如果子問題的規模足夠小,則停止遞迴,直接求解,最後將子問題的解組合成原問題的解。
對於最大子陣列,我們要尋求子陣列A[low…high]的最大子陣列。令mid為該子陣列的中央位置,我們考慮求解兩個子陣列A[low…mid]和A[mid+1…high]。A[low…high]的任何連續子陣列A[i…j]所處的位置必然是以下三種情況之一:
- 完全位於子陣列A[low…mid]中,因此
- 完全位於子陣列A[mid+1…high]中,因此
- 跨越了中點,因此
因此,最大子陣列必定為上述3種情況中的最大者。對於情形1和情形2,可以遞迴地求解,剩下的就是尋找跨越中點的最大子陣列。
任何跨越中點的子陣列都是由兩個子陣列A[i…mid]和A[mid+1…j]組成,其中且.因此,我們只需要找出形如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)
顯然這樣的分治演算法對於初學者來說,有點難度,但是熟能生巧, 多學多練也就不難了。該分治演算法的執行時間為
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,有:
這樣就有了一個子結構,對於初始情形,遍歷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
參考文獻
注意:本人現已開通兩個微信公眾號: 用Python做數學(微訊號為:python_math)以及輕鬆學會Python爬蟲(微訊號為:easy_web_scrape), 歡迎大家關注哦~~