一步步解读动态规划-合唱团

网易内推题里的合唱团见链接:https://www.nowcoder.com/questionTerminal/661c49118ca241909add3a11c96408c8

我们为了能说清楚动态规划的思路,下面先简化下合唱团的题:

  • 给定一个数组a[n],里面的数值代表能量值(都为正数),现在顺序抽取k(k>0)个能量值,约束条件是抽取的相邻能量值所在的索引不能超过d。求抽取k个能量的乘积最大值。

1、明确问题的目标是:

(1)求k个数的最大乘积

(2)相邻索引值差<=d

2、分解问题:

为了求解方便,我们来定个标准,根据k个数的末尾元素把目标问题分为以下n种情况

对以上n种情况比较结果,最后选取最大乘积输出,就是我们要求的目标值了。不过这样我们还需要求出每个结束元素前的k-1个数的最大乘积,这个新问题是不是似曾相识呢?求k-1个数的最大乘积同样又可以以刚才分析的方式去思考,以末尾元素为标准分为几种情况。

那么现在看来为了求目标问题,我们需要明确两个变量的值,一个是末尾元素,另一个是前k-1个最大乘积。因为我们求 k 个数最大乘积依赖于末尾元素和前k-1个数的最大乘积两个因素。我们定义一个二维数组q,行 i 代表末尾元素的索引值,列 j 代表长度。q[i][j] 表示以 a[i] 为末尾元素,长度为 j+1 的最大乘积值。是不是有点抽象了,没关系,接下来会一目了然的。

3、我们先拿个例子走下流程:a[5] = [4,5,2,1,7],k=3,d=2

状态数组q:

      j=  0   1  2

i=0  [ [   4                     ]

i=1    [   5  20        ]

i=2    [   2  10  40  ]

i=3    [   1  5  20  ]

i=4    [   7  14  70  ] ]

(1)先找到初始状态,k=1时,末尾元素为a[i],即q[i][0] = a[i],填充 q 数组。

(2)别忘了,我们有个约束条件,相邻索引值差<=d。假设长度为k的末尾元素索引值为i,它之前长度k-1的最后一个元素索引为p,那么 i-p<=d=2,即 max(i-2,0)=<p<i(i=0除外,因为i=0时末尾元素为a[0],长度只能是1)。

(3)有了初始状态,目标值还会远吗?

接下来看长度 k =2的情况 (即j=1,依赖于j=0的最大乘积和末尾元素),末尾元素为 a[0],长度只能为1,排除。

末尾元素为 a[1],前一个元素索引范围 0=<p<1,观察图1,前1个数最大乘积只能为 4。即q[1][1] = 4 * a[1] = 20 。

末尾元素为 a[2],前一个元素索引范围 0=<p<2,观察图2,前1个数最大乘积=max(4,5),即q[2][1] = 5*a[2] = 10。

末尾元素为 a[3],前一个元素索引范围 1=<p<3,观察图3,前1个数最大乘积=max(5,2),即q[3][1] = 5*a[3] = 5。

末尾元素为 a[4],前一个元素索引范围 2=<p<4,观察图4,前1个数最大乘积=max(2,1),即q[4][1] = 2*a[4] = 14。

(4)加油,还差一点点,你就成功了。

接下来看长度 k =3 (即j=2,依赖于j=1的最大乘积和末尾元素),末尾元素为 a[0],长度只能为1,排除。

末尾元素为 a[1],长度最多为2,排除。

末尾元素为 a[2],前一个元素索引范围 0=<p<2,p只能为1,观察图5,前2个数的最大乘积只能为20,所以q[2][2] = 20*a[2] = 40。

末尾元素为 a[3],前一个元素索引范围 1=<p<3,观察图6,前2个数的最大乘积=max(20,10),所以q[3][2] = 20*a[3] = 20。

末尾元素为 a[4],前一个元素索引范围 2=<p<4,观察图7,前2个数的最大乘积=max(10,5),所以q[4][2] = 10*a[4] = 70。

 

以上我们把状态数组q全部求出来了,不要因为走的太远而忘记为什么出发,我们的目标是比较末尾元素为a[i]的长度为k的最大乘积。那就比较q[i][2],得出70是以a[4]为末尾元素的长度为3的最大乘积。大致的思路就是这些了,原来求 k 个数的最大乘积要回溯到 k=1 时初始状态一步步求解啊。

搞明白了思路,代码也就出来了。就这个例子而言,代码如下:

 1 #coding:utf-8
 2 a = [4,5,2,1,7]
 3 k = 3
 4 d = 2
 5 n = len(a)
 6 q = [[0 for j in range(k)] for i in range(n)]
 7 #j代表列 i代表行 对状态数组q赋值
 8 value = 0
 9 for j in range(k):
10     for i in range(n):
11         #初始化第0列
12         if j == 0:
13             q[i][j] = a[i]
14             continue
15         for l in range(max(0,i-d),i):
16             q[i][j] = max(q[i][j], q[l][j-1]*a[i])
17         if j == k-1:
18             value = max(value, q[i][j])
19 print value

4、下面我们来总结动态规划

实际上动态规划问题都可以用递归来求解,不过递归的时候会求重复项并且造成栈溢出问题,所以我们借助状态数组把每个状态(递归返回值 )记录下来,用自底而下的思想去求解目标问题,从递归的边界条件出发求得动态规划的初始状态,然后一步步求解目标,相当于递归的逆过程。

动规解题的一般思路

(1) 原问题分解为子问题

  • 把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题从初始状态开始解决。

  • 子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。

(2)确定状态

  • 在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。

  • 所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。 在数字三角形的例子里,一共有N×(N+1)/2个数字,所以这个问题的状态空间里一共就有N×(N+1)/2个状态。

    整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。

(3)确定一些初始状态(边界状态)的值

以上面为例,就是求解q数组第一列的值。

(4) 确定状态转移方程

     定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。

特点:

    1>问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。

    2>无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

引申:想到HMM里的维特比算法也是利用动态规划求解最短路径,有必要再加强学习下!

 

参考:http://blog.csdn.net/baidu_28312631/article/details/47418773

 

原文地址:https://www.cnblogs.com/hithink/p/7380840.html