本文主要总结了引文中提出的金矿模型的思考方法,并用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
[egin{eqnarray}f(pn,mn)=egin{cases}0,&mn = 0 且 pn < npn[mn]cr gn[mn],&mn = 0 且 pn >= npn[mn]cr
max{f(pn,mn - 1),gn[mn] + f(pn - npn[mn], mn - 1)},&otherend{cases}end{eqnarray}]
分析:
- 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