1. 程式人生 > >動態規劃法(三)子集和問題(Subset sum problem)

動態規劃法(三)子集和問題(Subset sum problem)

慢慢 們的 遞歸法 found swe otto 題目 子集和問題 自己的

??繼續講故事~~
??上次講到我們的主人公丁丁,用神奇的動態規劃法解決了雜貨店老板的兩個找零錢問題,得到了老板的肯定。之後,他就決心去大城市闖蕩了,看一看外面更大的世界。
??這天,丁丁剛回到家,他的弟弟小連就攔住了他,“老哥,有個問題想請教你。”對於一向數學見長的小連,這次竟然破天荒的來問自己問題,丁丁感到不可思議:他倆一個以計算機見長,一個以數學見長,各自心裏都有點小驕傲,不會輕易地向對方問問題。丁丁遲疑了一會兒,慢慢說道:“有什麽問題是我們數學小天才解決不了的?”
??原來小連剛上高一,正在學數學中的集合,這不,今天他從一本算法書上看到一道題,想了很久都沒有想出來。他把題目給了丁丁看:

對於某個給定值M,如何從某個給定的正整數集合S中找個一個子集合s,使得該子集和為給定值M。如M=7,S={1,3,4,5},則s={3,4}.

??看到這道題目,丁丁腦海中掠過“動態規劃法”的念頭,對於動態規劃法,他已經是輕車熟路了,但是對於究竟能否用動態規劃法解決這個問題,他一時也沒主意。於是,他對小連說道:“這題也許可以用動態規劃法解決,不過我得好好想一想。”小連點點頭,他還是蠻相信他的哥哥的。
??丁丁走進自己的房間,拿出草稿紙,開始了思考的旅程:

對於S={a1,a2,...,an},每個元素只有取與不取兩種情況,再考慮它們的和是否等於M,但是這樣的情況共有2^n中,這種算法的效率顯然是不行的。

??換條思路,令subset(i,j)表示S中前i個元素的子集和等於j的情況,則

  • 若S[i] > j,則S[i]不在子集s中。
  • 若S[i] <= j, 則有以下兩種情況:一種情況是S[i]不在子集s中,則subset(i, j) = subset(i-1, j); 一種情況是S[i]在子集s中,則subset(i, j)= subset(i-1, j-S[i]).

??這樣就有了這個問題的子結構問題,因此,只需要確定初始情況即可:

對於i=0,1,2,...,n,有subset(i, 0)=True, 對於j=1,2,...,M, 有subset(0, j)=False.

因此,利用動態規劃法,就能得到(n+1)*(M+1)的真值表了,而答案就是subset(n, M). 算法有了,Python代碼自然也有了:

import numpy as np

# A Dynamic Programming solution for subset sum problem
# Returns true if there is a subset of set with sum equal to given sum

def isSubsetSum(S, n, M):
    # The value of subset[i, j] will be
    # true if there is a subset of
    # set[0..j-1] with sum equal to i
    subset = np.array([[True]*(M+1)]*(n+1))

    # If sum is 0, then answer is true
    for i in range(0, n+1):
        subset[i, 0] = True

    # If sum is not 0 and set is empty,
    # then answer is false
    for i in range(1, M+1):
        subset[0, i] = False

    # Fill the subset table in bottom-up manner
    for i in range(1, n+1):
        for j in range(1, M+1):
            if j < S[i-1]:
                subset[i, j] = subset[i-1, j]
            else:
                subset[i, j] = subset[i-1, j] or subset[i-1, j-S[i-1]]

    # print the True-False table
    for i in range(0, n+1):
        for j in range(0, M+1):
            print(‘%-6s‘%subset[i][j], end="  ")
        print(" ")

    if subset[n, M]:
        print("Found a subset with given sum")
    else:
        print("No subset with given sum")

# test
st = [1, 3, 4, 5]
n = len(st)
sm = 7
isSubsetSum(st, n, sm)

輸出結果如下:

True False False False False False False False
True True False False False False False False
True True False True True False False False
True True False True True True False True
True True False True True True True True
Found a subset with given sum

??那麽,怎樣求解子集s中的元素呢?也許可以用回溯法(backtracing),他這樣想到,不過,他還是決定把剩余部分交給弟弟小連。
??幾分鐘後,當小連看到丁丁的解法後,興奮地直跳起來。對於計算機編程,他也是有相當大的興趣的,不過當務之急是解決哥哥剩下來的問題,那就是找出s中的元素。他想試著從輸出的真值表入手:

對於subset(i, j) = subset(i-1, j)=True,則元素S[i]不在子集s中。對於subset(i,j)=True而subset(i-1, j)=False,則元素S[i]必定在子集s中, 此時subset(i-1, j-S[i])=True,這樣就能通過遞歸法找到s中的元素了。對於這個問題,只要從subset(n, M)開始即可。

他覺得自己的思路是可行的,於是就在哥哥的程序上修改了起來:

import numpy as np

# A Dynamic Programming solution for subset sum problem
# Returns true if there is a subset of set with sum equal to given sum

def isSubsetSum(S, n, M):
    # The value of subset[i, j] will be
    # true if there is a subset of
    # set[0..j-1] with sum equal to i
    subset = np.array([[True]*(M+1)]*(n+1))

    # If sum is 0, then answer is true
    for i in range(0, n+1):
        subset[i, 0] = True

    # If sum is not 0 and set is empty,
    # then answer is false
    for i in range(1, M+1):
        subset[0, i] = False

    # Fill the subset table in bottom-up manner
    for i in range(1, n+1):
        for j in range(1, M+1):
            if j < S[i-1]:
                subset[i, j] = subset[i-1, j]
            else:
                subset[i, j] = subset[i-1, j] or subset[i-1, j-S[i-1]]

    # print the True-False table
    for i in range(0, n+1):
        for j in range(0, M+1):
            print(‘%-6s‘%subset[i][j], end="  ")
        print(" ")

    if subset[n, M]:
        print("Found a subset with given sum")
        sol = []
        # using backtracing to find the solution
        i = n
        while i >= 0:
            if subset[i, M] and not subset[i-1, M]:
                sol.append(S[i-1])
                M -= st[i-1]
            if M == 0:
                break
            i -= 1
        print(‘The solution is %s.‘ % sol)
    else:
        print("No subset with given sum")

# test
st = [1, 3, 4, 5]
n = len(st)
sm = 7
isSubsetSum(st, n, sm)

輸出結果如下:

True False False False False False False False
True True False False False False False False
True True False True True False False False
True True False True True True False True
True True False True True True True True
Found a subset with given sum
The solution is [4, 3].

??終於解決了這個問題,小連長舒一口氣,而站在一旁的丁丁,看著弟弟的程序,也露出了滿意的微笑~~
??晚飯後,哥倆正坐在門口的大樹下乘涼,一旁的大雄急匆匆地跑過來來他倆幫忙。原來,他也碰到了一道難題,題目是這樣的:

對於一個由若幹個正整數組成的集合S,如何將S劃分成兩部分,使得兩部分的和一樣?

丁丁和小連看了題目,微微一笑,因為答案就在他們剛才解決的問題中。那麽,親愛的讀者,你能嘗試著解決這道問題嗎?

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

動態規劃法(三)子集和問題(Subset sum problem)