1. 程式人生 > >動態規劃法(八)最大子陣列問題(maximum subarray problem)

動態規劃法(八)最大子陣列問題(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,暴力求解方法的思路是很簡單的,就是將子陣列的開始座標和結束座標都遍歷一下,這樣共有Cn2中組合方式,再考慮這所有組合方式中和最大的情形即可。
  該演算法的執行時間為O(n2),效率是很低的。那麼,還有其它高效的演算法嗎?

分治法

  分治法的基本思想是將問題劃分為一些子問題,子問題的形式與原問題一樣,只是規模更小,遞迴地求解出子問題,如果子問題的規模足夠小,則停止遞迴,直接求解,最後將子問題的解組合成原問題的解。
  對於最大子陣列,我們要尋求子陣列A[low…high]的最大子陣列。令mid為該子陣列的中央位置,我們考慮求解兩個子陣列A[low…mid]和A[mid+1…high]。A[low…high]的任何連續子陣列A[i…j]所處的位置必然是以下三種情況之一:

  1. 完全位於子陣列A[low…mid]中,因此lowijmid.
  2. 完全位於子陣列A[mid+1…high]中,因此mid<ijhigh.
  3. 跨越了中點,因此lowimid<jhigh.

因此,最大子陣列必定為上述3種情況中的最大者。對於情形1和情形2,可以遞迴地求解,剩下的就是尋找跨越中點的最大子陣列。
  任何跨越中點的子陣列都是由兩個子陣列A[i…mid]和A[mid+1…j]組成,其中lowimidmid<

jhigh.因此,我們只需要找出形如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(nlogn).

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[i1],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 

參考文獻

注意:本人現已開通兩個微信公眾號: 用Python做數學(微訊號為:python_math)以及輕鬆學會Python爬蟲(微訊號為:easy_web_scrape), 歡迎大家關注哦~~