2022年 11月 9日

《你也能看得懂的Python算法书》学习笔记(十一)

从本篇学习笔记开始,我们进入了动态规划算法的学习中。

动态规划算法将待求解问题拆分成一系列互相交叠的子问题,通过递推关系定义各子问题的求解策略,并随时记录子问题的解,最终获得原始问题的解,避免了对交叠子问题的重复求解。

在动态规划算法中有三要素,及最优子结构、边界和状态转移方程。最优子结构是指每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到;边界是指问题最小子集的解;状态转移函数是指从一个阶段向另一个阶段过渡的具体模式,描述的是两个相邻子问题之间的关系。

具备以上三要素的问题均可以采用动态规划的策略进行求解。

我们先来看一道题目:爬楼梯问题

题目描述:

小明回家需要爬一个有十层台阶的楼梯,他每次可以选择走一级台阶或者走两级台阶。请问小明回家一共有多少种走法。

这道题如果我们正向思考反而不太好求解,我们可以试想一下:如果小明要上到第十级台阶,那么他最后一步一定是在第八级台阶或者第九级台阶。也就是说,如果我们知道了小明上到第八级台阶有X种走法,上到第九级台阶有Y种走法,那么他上到第十级台阶就有(X+Y)种走法。

为了方便描述,我们使用F(n)来表示第n级台阶的走法数量,根据以上分析我们可以得到F(10)=F(9)+F(8)。以此类推,F(n)=F(n-1)+F(n-2)。当n=1和2时,F(1)=1,F(2)=2。

分析到这里,该问题作为动态规划求解的三个要素已经全部出现了。

边界:F(1)=1,F(2)=2

最优子结构:F(n)的最优子结构是F(n-1)和F(n-2)

状态转移函数:F(n)=F(n-1)+F(n-2)

之后我们实用程序来实现它,我使用了递归函数,代码如下:

  1. def upstairs(n):
  2. if n < 1:
  3. return 0
  4. if n == 1:
  5. return 1
  6. if n == 2:
  7. return 2
  8. if n > 2:
  9. return upstairs(n - 1) + upstairs(n - 2)
  10. a = input("请输入台阶数:")
  11. print(upstairs(int(a)))

做完上面的入门例题之后,我们开始解决一道动态规划经典例题——矿工挖矿问题。

矿工挖矿问题是为了解决在给定金矿和矿工数量的前提下,能够获得最多黄金的挖矿策略。

问题描述:

某地区发现了五座金矿,没座金矿的黄金储量不同,需要参与挖掘的工人数也不同。假设参与挖掘工人的总数是10人,且每座金矿要么全挖,要么不挖,不能派出一半人挖去一半金矿,要求用程序求解,要想尽可能多的获得黄金,应该选择挖取哪几座金矿。

金矿编号 黄金储量 所需工人数量
1

400 

5
2 500 5
3 200 3
4 300 4
5 350 3

如果要用动态规划算法解决该问题,需要定位用于解题的三要素:最优子结构、边界和状态转移函数。

首先我们寻找最优子结构。我们解题的目标是确定10个工人挖5座金矿时能够获得的最多的黄金数量,该结果可以从10个工人挖四座金矿的子问题中递归求解。

在解决十个工人挖四座金矿的过程中,存在两种选择,一种是放弃第五座金矿,将10个工人全部投放到前4座金矿的挖掘中。该情况如下图所示:

 另一种选择是对第五座金矿进行挖掘,因此需要从10人分配3人加入第五座金矿的挖掘。该情况如下图所示:

因此,最终的最优解应该是这两种选择中获得黄金数量较多的那个,即为上两图所示场景的最大值。

为了方便描述,我们定义一下变量,假设金矿的数量为n,工人的数量为w,当前获得的黄金的数量为P[n],根据上述分析,要获得10个矿工挖掘第5座金矿的最优解F(5,10),需要在F(4,10)和F(4,10-P[5])+G[5])中获取较大的值,即

F(5,10) = max(F(4,10),F(4,10-P[5])+G[5])

因此,针对该问题而言,以上便是 F(5,10)情况下的最优子结构。

之后,我们来考虑该问题的边界。对于一座金矿的情况,若当前的矿工数量不能满足该金矿的挖掘需要,则获得的黄金数量为0,若能满足矿工数量要求,则获得的黄金数量为G[n]。因此,该问题的边界条件可表达为:

当n = 1,w >= P[n]时,F(n,w) = G[n];

当n = 1,w < P[n]时,F(n,w) = 0

综上,可以得到该问题的状态转移函数:

F(n,w) = 0(n <= 1,w < p[n])

F(n,w) = G[n](n == 1,w >= P[n])

F(n,w) = F(n-1,w)(n > 1,w < P[n])

F(n,w) = max(F(n – 1,w),F(n – 1,w – P[n]) + G[n])(n > 1,w >= P[n])

至此,定义了用动态规划算法解决该问题的三个要素,下面要做的是利用边界、最优子结构和状态转移函数对该问题进行求解。 

初始化阶段,利用表格求解思路。最终的表格如下:

1人 2人 3人 4人 5人 6人 7人 8人 9人 10人
1金矿 0 0 0 0 400 400 400 400 400 400
2金矿 0 0 0 0 500 500 500 500 500 900
3金矿 0 0 200 200 500 500 500 700 700 900
4金矿 0 0 200 300 500 500 500 700 800 900
5金矿 0 0 350 350 500 550 650 850 850 900

观察图标我们可以发现,除了第一座金矿的相关数据,表格中的其他数据都可以由前一行的一个或两个格子中的数推导而来。例如,3座金矿8个人挖掘的黄金量F(3,8)就来自2个金矿8个人挖掘的黄金量F(2,8)和2个金矿5个人挖掘的黄金量F(2,5),即F(3,8) = max(F(2,8),F(2,5) + 200) = max(500,500+200)=700

再比如,4座金矿10个人挖掘的黄金量F(4,10),来自3座金矿10个人挖掘的黄金量F(3,10)和3座金矿6个人挖掘的黄金量F(3,6),即F(4,10) = max(F(3,10),F(3,6) + 300) = max(900,500+300)=900

 根据以上的思路,在用程序实现该算法的过程中,采用自底向上的方式进行计算,像填表过程一样从左至右,从上到下逐渐获得计算结果。这样,可以不需要存储整个表格的内容仅需要存储前一行的结果,就可以推导出下一行的内容。

下面我们来编写一下程序。

首先我们定义函数goldMining(n,w,g,p)来计算挖掘第n座金矿、有w个参与时能获得的黄金量,其中g和p分别为数组,分别存放对应各座金矿的黄金量以及所需的工人数。在正式迭代之前,首先界定边界的情况。当工人数量小于p[0]时,说明目前工人数量不能开始任何金矿的挖掘,获得黄金数量为0;当工人数量大于等于p[0]时,获得的黄金数量即为g[0]。

  1. def goldMining(n, w, g, p):
  2. # 第n座金矿,w个工人,g为数组(存放各座金矿的黄金量),p存放各座金矿所对应的工人数
  3. result = []
  4. preResult = []
  5. for i in range(0,w):
  6. if i < p[0]:
  7. preResult[i] = 0
  8. else:
  9. preResult[i] = g[0]
  10. for i in range(0,n):
  11. for j in range(0,w):
  12. if j < p[i]:
  13. result[j] = preResult[j]
  14. else:
  15. result[j] = max(preResult[j],preResult[j - p[i]] + g[i])
  16. preResult = result
  17. return result

之后即可进入循环的迭代过程。在迭代中,根据之前的分析,我们仅需要关注前一行preResults的取值,即可通过状态转移函数F(n,w) = F(n-1,w)(n > 1,w < P[n])或F(n,w) = max(F(n - 1,w),F(n - 1,w - P[n]) + G[n])(n > 1,w >= P[n])来获得F(n,w)的值,因此,整个迭代过程进需要引入preResults数组保存前一行的值即可。

最终代码如下:

  1. def goldMining(n, w, g, p):
  2. # 第n座金矿,w个工人,g为数组(存放各座金矿的黄金量),p存放各座金矿所对应的工人数
  3. result = []
  4. preResult = []
  5. result.append(0)
  6. preResult.append(0)
  7. for i in range(1,w+1):
  8. result.append(0)
  9. if i < p[0]:
  10. preResult.append(0)
  11. else:
  12. preResult.append(g[0])
  13. result[i] = preResult[i]
  14. for i in range(0,n):
  15. for j in range(0,w):
  16. if j < p[i]:
  17. result[j] = preResult[j]
  18. else:
  19. result[j] = max(preResult[j],preResult[j - p[i]] + g[i])
  20. preResult = result
  21. return result
  22. for i in range(1,n):
  23. for j in range(1,w+1):
  24. if j < p[i]:
  25. result[j] = preResult[j]
  26. else:
  27. result[j] = max(preResult[j],preResult[j-p[i]] + g[i])
  28. preResult = result.copy()
  29. del result[0]
  30. return result
  31. if __name__ == "__main__":
  32. n = 5
  33. w = 10
  34. g = [400,500,200,300,350]
  35. p = [5,5,3,4,3]
  36. result = goldMining(n,w,g,p)
  37. print(result)