1. 程式人生 > >演算法優化:最大m個子段和,問題規模從1個子段和擴充套件到m個,動態規劃

演算法優化:最大m個子段和,問題規模從1個子段和擴充套件到m個,動態規劃

最大m個子段和,問題規模從1個子段和擴充套件到m個,動態規劃

問題規模由2個決定,一是子段數m,二是元素個數n,準確的說是最後一個子段終止的標號

b(i,j)定義為:前j個元素中有i個子段,且第i個子段包含j,i個子段和為b(i,j)
那麼原問題的最優解為max{b(m,j)},m<=j<=n
還是不是針對原問題動態規劃,動態規劃的問題是可能解,最後在可行解裡面找最優解

根據b(i,j)定義,有兩種情況,第一種情況arr[i]是第i個子段的一部分,那前面j-1的尾巴也是屬於第i個子段,可以把前面j-1也看成有i個子段,即b(i,j-1) + arr[j];另外一種情況arr[i]就是第i個子段的全部,也就是前面j-1的尾巴不屬於第i個子段,那我們必須在前j-1個元素裡找到i-1個子段使之和最大(類似原始問題max{b(m,j)},m<=j<=n),所以我們這裡是max{b(i-1,k)},i-1<=k<=j-1,即第二種情況為:max{b(i-1,k)} + arr[j] ,i-1<=k<=j-1

狀態方程為:

在這裡插入圖片描述

程式碼實現如下:

#%%
def maxMSum(arr,m):
    n = len(arr)
    b = np.zeros((m+1,n+1),int)
    
    for i in range(1,m+1):
        # (i,n+1-m+i),本來是(i,n+1),可以發現有些沒有必要計算,
        # 因為[i][j]總是取左邊或者左上的資料     
        for j in range(i,n+1-m+i):
            # 最後剛好子段個數==元素個數時,就是相當於一個元素一個子段
            if
j == i: b[i][j] = b[i-1][j-1] + arr[j-1] # 其他的情況需要按照狀態方程比較 else: # 第一種情況 b[i][j] = b[i][j-1] + arr[j-1] # 第二種情況 for k in range(i-1,j): if b[i-1][k] + arr[j-1] > b[i]
[j]: b[i][j] = b[i-1][k] + arr[j-1] # 最後一行都是可行解,取最大為最優解 return b,max(b[m][:]) arr = [-2,11,-4,-4,13,-5,-2,1,1,5,8,-1] print(maxMSum(arr,3)) (array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [ 0, -2, 11, 7, 3, 16, 11, 9, 10, 11, 16, 0, 0], [ 0, 0, 9, 7, 7, 24, 19, 17, 18, 19, 24, 32, 0], [ 0, 0, 0, 5, 5, 22, 19, 22, 25, 26, 31, 39, 38]]), 39)

從程式碼或者從狀態方程我們可以看出

在這裡插入圖片描述

只是使用了兩行b[i-1][:j]和b[i][j-1]資料,可以利用滾動陣列優化,如何優化了?從這裡看至少需要2行才行。

b[i-1][:j]用於求max{b(i-1,k)} + arr[j],我們可以使用一個數組c專門記錄max{b(i-1,k)} ,c[j-1]代表k從i-1到j-1裡面最大值,b[i][j-1]直接使用當前的陣列b[j-1]就行了,當前行b[j] = max{c[j-1],b[j-1]} + arr[j], 此時還需要做一件事,時刻記錄b到目前位置j的最大值,把b當前最大值cur_max一直放在空中,一旦得到一個新的b大於空中的值的話更新空中的值cur_max,這個值代表的時0到j的最大值,把更新之前的cur_max放到c[j-1](記住這一層b[j+1]待會兒用的是c[j]),留給下一層也就是i+1層使用,i+1層的b[j] 用的是上一層留下的c[j-1]。

這樣使用滾動陣列節約了空間消費,使用臨時cur_max一輪比較就把一行的最大值都更新了,避免後續每一次都用for迴圈算一遍。

程式碼實現如下:

def maxMSum_opt(arr,m):
    n = len(arr)
    b = [0]*(n+1)
    # 陣列c專門記錄max{b(i-1,k)} ,c[j-1]代表k從i-1到j-1裡面最大值,需要初始化
    # 參考b的第0行可以得到c的初始化如下
    c= [0]*(n+1)
    
    for i in range(1,m+1):
        # 先把第一個元素處理一下,也就是j == i,我們只使用一個一維的滾動陣列
        b[i] = b[i-1] + arr[i-1]
        # 我們需要一個變數記錄當前一行的到當前位置的最大值
        cur_max = b[i]
        
        # (i,n+1-m+i),本來是(i,n+1),可以發現有些沒有必要計算,
        # 因為[i][j]總是取左邊或者左上的資料     
        # 第一個元素上面已經處理了
        for j in range(i+1,n+1-m+i):
            # 獲取b[j]當前的值
            b[j] = max(b[j-1],c[j-1]) + arr[j-1]
            # c[j-1] 使用了之後,就要更新c[j-1]留給下一層使用,值為還未更新的cur_max
            c[j-1] = cur_max
            # 更新後的cur_max要放在c[j],但是舊的c[j]還沒使用了,所以c[j]的更新是滯後一步的
            if cur_max <  b[j]:
                cur_max = b[j]
        # 我們會發現記憶體迴圈完了,最後的cur_max還沒更新到c裡面,因為c的更新是滯後一步
        # 得再次進入迴圈才能更新的,注意這裡c的標號
        c[n-m+i] = cur_max
           

    # 最後一行都是可行解,取最大為最優解
    return b,max(b[:])


arr = [-2,11,-4,-4,13,-5,-2,1,1,5,8,-1]
print(maxMSum_opt(arr,3))         


([0, -2, 9, 5, 5, 22, 19, 22, 25, 26, 31, 39, 38], 39)