1. 程式人生 > >動態規劃系列(1)——金礦模型的理解

動態規劃系列(1)——金礦模型的理解

本文主要總結了引文中提出的金礦模型的思考方法,並用python程式碼來實現演算法,從而加深了對動態規劃思想的理解。

1. 故事描述

注:內容節選自開篇引文。

有一個國家,所有的國民都非常老實憨厚,某天他們在自己的國家發現了十座金礦,並且這十座金礦在地圖上排成一條直線,國王知道這個訊息後非常高興,他希望能夠把這些金子都挖出來造福國民,首先他把這些金礦按照在地圖上的位置從西至東進行編號,依次為0、1、2、3、4、5、6、7、8、9,然後他命令他的手下去對每一座金礦進行勘測,以便知道挖取每一座金礦需要多少人力以及每座金礦能夠挖出多少金子,然後動員國民都來挖金子。

  • 題目補充1:挖每一座金礦需要的人數是固定的,多一個人少一個人都不行。國王知道每個金礦各需要多少人手,金礦i需要的人數為peopleNeeded[i]。
  • 題目補充2:每一座金礦所挖出來的金子數是固定的,當第i座金礦有peopleNeeded[i]人去挖的話,就一定能恰好挖出gold[i]個金子。否則一個金子都挖不出來。
  • 題目補充3:開採一座金礦的人完成開採工作後,他們不會再次去開採其它金礦,因此一個人最多隻能使用一次。
  • 題目補充4:國王在全國範圍內僅招募到了10000名願意為了國家去挖金子的人,因此這些人可能不夠把所有的金子都挖出來,但是國王希望挖到的金子越多越好。
  • 題目補充5:這個國家的每一個人都很老實(包括國王),不會私吞任何金子,也不會弄虛作假,不會說謊話。
  • 題目補充6:有很多人拿到這個題後的第一反應就是對每一個金礦求出平均每個人能挖出多少金子,然後從高到低進行選擇,這裡要強調這種方法是錯的,如果你也是這樣想的,請考慮揹包模型,當有一個揹包的容量為10,共有3個物品,體積分別是3、3、5,價值分別是6、6、9,那麼你的方法取到的是前兩個物品,總價值是12,但明顯最大值是後兩個物品組成的15。
  • 題目補充7:我們只需要知道最多可以挖出多少金子即可,而不用關心哪些金礦挖哪些金礦不挖。

那麼,國王究竟如何知道在只有10000個人的情況下最多能挖出多少金子呢?

2. 問題抽象

注:內容節選自開篇引文。

輸入檔名為“gold.in”。

輸入檔案第一行有兩個數M和N,M是國王可用用來開採金礦的總人數,N是總共發現的金礦數。

輸入檔案的第2至N+1行每行有兩個數,第i行的兩個數分別表示第i-1個金礦需要的人數和可以得到的金子數。

輸出檔案僅一個整數,表示能夠得到的最大金子數。

輸入樣例:

100 5

77 92

22 22

29 87

50 46

99 90

輸出樣例:

133

3. 分析

按照原部落格中的思路進行分析,定義如下幾個陣列和函式:

  • needed_people_num[i]:表示開採金礦i(注意:i從0開始,0 <= i < N,下同)所需的人數。
  • gold_num[i]:表示開採金礦i能獲取到的金子數。
  • f(people_num,mine_num):表示當用people_num個人挖第0~mine_num座金礦可以獲取到的最大金子數。

狀態轉移方程如下:
注:其中將people_num簡記為:pn,mine_num簡記為:mn,needed_people_num[]簡記為:npn[],gold_num簡記為:gn

f(pn,mn)=0,gn[mn],max{f(pn,mn1),gn[mn]+f(pnnpn[mn],mn1)},mn=0pn<npn[mn]mn=0pn>=npn[mn]other

分析:

  • mine_num = 0是邊界條件,也是遞迴的結束條件。
  • 因為有很多次遞迴會重複計算,所以考慮採用一個二維快取陣列(備忘錄)來儲存每次計算的結果值。

4. Talk is cheap, show you the code

# coding:utf-8

# 可支配的總人數
total_people_num = 0
# 金礦總數
total_mine_num = 0
# 開採每個金礦所需的人數列表
needed_people_num = []
# 開採每個金礦可以獲取到的金子數列表
gold_num = []
# 快取陣列,是一個二維陣列,有total_people_num+1行,total_mine_num+1列
# 使用快取陣列是為了減少重複計算,cache[i][j]表示用i個人開採第0~j座金礦一共能開採到的金子總數最大值
# cache的所有元素都在一開始被初始化為-1,表示未知
# cache的使用可以極大地提高效率,減少很多重複計算
cache = []

# 初始化資料
def init():
    global total_people_num
    global total_mine_num
    global needed_people_num
    global gold_num
    global cache

    # 從gold.in檔案中讀取資料
    '''
    檔案內容:
    100 5
    77 92
    22 22
    29 87
    50 46
    99 90
    '''
    datas = []
    with open('gold.in','r') as f_in:
        datas = f_in.readlines()

    # 解析出總人數和金礦數
    line1 = datas[0]
    total_people_num = int(line1.split()[0])
    total_mine_num = int(line1.split()[1])

    # 解析出needed_people_num和gold_num列表
    for line in datas[1:]:
        needed_people_num.append(int(line.split()[0]))
        gold_num.append(int(line.split()[1]))

    # 初始化cache陣列,下面這種方式是有問題的:
    # cache_row = [-1] * (total_mine_num + 1)
    # cache = [cache_row] * (total_people_num + 1)
    # 要用這種方式(為了防止陣列越界,都加了1):
    cache = [([-1] * (total_mine_num + 1)) for i in range(total_people_num + 1)]

# 求最大金子數的函式
# get_max_gold_num(i,j)表示用i個人開採第0~j座金礦可以開採到的最大金子總數
def get_max_gold_num(people_n,mine_n):
    max_num = 0

    # 1. 如果快取陣列中有對應值,直接從中取
    if cache[people_n][mine_n] != -1:
        max_num = cache[people_n][mine_n]
    # 2. 如果只開採第0座金礦
    elif mine_n == 0:
        # 2.1 如果人數小於開採第0座金礦所需人數,那麼結果就是0
        if people_n < needed_people_num[mine_n]:
            max_num = 0
        # 2.2 否則最終結果就是開採第0座金礦所能獲取到的金子數     
        else:
            max_num = gold_num[mine_n]
    # 3. 如果不是第0座金礦且人數足夠開採第mine_n座金礦,那麼取下面兩種開採策略所能獲取到的最大金子數的較大值
    elif people_n >= needed_people_num[mine_n]:
        # 用people_n個人去開採第0~mine_n - 1座金礦所能獲取到的最大金子數
        m = get_max_gold_num(people_n,mine_n - 1)
        # 用people_n個人去開採第0~mine_n座金礦所能獲取到的金子數的最大值
        n = gold_num[mine_n] + get_max_gold_num(people_n - needed_people_num[mine_n],mine_n - 1)
        max_num = max(m,n)
    # 4. 如果不是第0座金礦且人數不夠開採第mine_n座金礦,那隻能採取第一種策略了:使用people_n個人開採其他的mine_n - 1座金礦
    else:
        max_num = get_max_gold_num(people_n,mine_n - 1)

    # 5. 給快取陣列對應元素賦值
    cache[people_n][mine_n] = max_num
    return max_num

def main():
    init()
    print get_max_gold_num(total_people_num,total_mine_num - 1)

main()

輸出結果:
133