算法 | Five Steps to Dynamic Programming(解决动态规划问题的五个步骤)

动态规划(DP)是最常用的算法之一。它借助Divide and Conquer的思想,将一个问题分解为一个个渐进的subproblems(子问题),最终通过解决这些子问题来得到问题的终极答案。
和recursion(递归)的不同,DP会将所有子问题的答案存在一个表格里,以达到方便提取、提升效率的作用。因此,用递归需要指数时间解决的问题,很多都可以用DP在多项式时间内解决。一个最常见的例子就是计算第n项斐波那契数列。

解决动态规划问题的五个步骤如下:

一、判断问题是否能用动态规划算法解决

一般来说,动态规划问题都可以写成如下形式:
max ⁡ f ( x 1 , x 2 , . . . x n ) s . t . L ( y 1 , y 2 , . . . , y n ) < 0 \max f(x1,x2,...xn) s.t. L(y1,y2,...,yn)<0 maxf(x1,x2,...xn)s.t.L(y1,y2,...,yn)<0
where L(y1,y2…yn) is the constraint function
即,在某些限制条件之下求出目标函数(objective function)的最大值或最小值
用Max Array Sum问题来举例说明:输入一个数列arr,这道题的目标是在arr中选择若干两两不相邻的数,使它们的和最大。这道题就是典型的动态规划,限制条件是必须选择不相邻的数,目标函数则是它们的和。

二、写出子问题(subproblem)

将目标函数表示为OPT(n),那么子问题一般可以表示为OPT(k),其中k是input的大小。
继续以Max Array Sum问题为例。input长度为n的数列arr,我们最终想要算出的是:
O P T ( n ) = a r r 中 满 足 限 制 条 件 的 数 之 和 的 最 大 值 OPT(n) = arr中满足限制条件的数之和的最大值 OPT(n)=arr
子问题可以表示为:
O P T ( k ) = a r r [ 0 : k ] 中 满 足 条 件 的 数 之 和 的 最 大 值 OPT(k) = arr[0:k]中满足条件的数之和的最大值 OPT(k)=arr[0:k]
也就是说,子问题解决的是arr前k项的Max Array Sum问题。

三、写出Recurrence(递归函数)

假设我们知道OPT(1)~OPT(k)的答案,如何用它们算出OPT(k+1)?答案是递归函数,连接子问题和目标函数之间的桥梁。 递归函数的一般形式是:
O P T ( k + 1 ) = f ( O P T ( 1 ) , O P T ( 2 ) , . . . , O P T ( k ) ) OPT(k+1) = f( OPT(1),OPT(2),...,OPT(k) ) OPT(k+1)=f(OPT(1),OPT(2),...,OPT(k))
找到递归函数是动态规划中最重要,也是最有难度的一步。我们一般需要通过分类讨论来穷尽所有可能性。
在Max Array Sum中,OPT(k+1)将考虑的数列从前k项增加为前k+1项。这时,有两种可能性:1)数列第k+1项不在最优结果里;2)数列第k+1项在最优结果里。如果是前者,那么OPT(k+1) = OPT(k);如果是后者,那么既然第k+1项在最优结果里,根据限制条件,第k项就不会在最优结果里。也就是说,此时OPT(k+1)与OPT(k-1)和arr[k]有关。这时又有两种可能性:1)OPT(k+1) = OPT(k-1) + arr[k];2)OPT(k-1)是负数,因此OPT(k+1)直接等于arr[k]。
总结以上分析,我们可以写出这道题的递归函数:
O P T ( k + 1 ) = max ⁡ O P T ( k ) , O P T ( k − 1 ) + a r r [ k ] , a r r [ k ] ) OPT(k+1) = \max OPT(k), OPT(k-1)+arr[k], arr[k] ) OPT(k+1)=maxOPT(k),OPT(k1)+arr[k],arr[k])
即,选择三种可能性中的最大值作为最优解。

四、确定Base Case(终止条件)

和递归一样,写出程序停止的条件。一般而言这个条件都是取n=1。
Max Array Sum中的base case显而易见:
O P T ( 1 ) = a r r [ 0 ] , O P T ( 2 ) = max ⁡ ( a r r [ 0 ] , a r r [ 1 ] ) OPT(1) = arr[0],OPT(2) = \max(arr[0],arr[1]) OPT(1)=arr[0]OPT(2)=max(arr[0],arr[1])

五、代码实现

代码实现有两种思路:Memoization和Tabulation。
简单理解,memoization等于top down approach(从上至下):我要计算OPT(n),因此需要算出OPT(1), OPT(2),… 这个思路类似递归。而tabulation则是bottom up approach(由下至上):第一步算出OPT(1),再算出OPT(2),…最后算出OPT(n)。
从逻辑角度,memoization更加直接易懂,但是从效率角度我个人更喜欢tabulation。memoization在执行过程中会积压大量指令,因此在n较大的时候可能会造成较高的空间浪费。
Max Array Sum问题采取tabulation的代码实现如下:

#arr is the input array
def maxSubsetSum(arr):
    k=len(arr)
    dic={}
    dic[1]=arr[0]            #Base case
    if k>1:
        dic[2]=max(arr[0],arr[1])    
        for k in range(3,len(arr)+1):
            dic[k]=max(dic[k-1],dic[k-2]+arr[k-1],arr[k-1])    #Recurrence
    return dic[k] 

你可能感兴趣的:(Python,算法)