动态规划系列(1)——金矿模型的理解

参考:http://www.cnblogs.com/sdjl/articles/1274312.html

本文主要总结了引文中提出的金矿模型的思考方法,并用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简记为:pnmine_num简记为:mnneeded_people_num[]简记为:npn[]gold_num简记为:gn

分析:

  • 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容