动态规划

117 阅读4分钟

动态规划思想

动态规划是通过拆解待解决问题,通过子问题间状态的关系,以递推的方式完成整个过程的推算。

主要方式是将问题分解成若干个子问题,按照顺序求解每个子问题,而前一个子问题的解为后一个子问题提供有用信息。

在动态规划中,每个子问题的解通常保存在数组里,供面后续问题计算时使用,避免重复计算。

动态规划方法

已知一个规模为n,前提为A的问题,求解未知解B。

  • 如果将A的规模降低到0,可以得到B,有A0->B;
  • 如果A0添加一个元素可以得到A1,即A0->A1, 可以递推得到A1->A2,A2->A3,...,Ai-1->Ai。

对于Ai,只需要上一个状态Ai-1即可以完成整个过程的推导,这个推理过程为“贪心法”。 而很多时候,Ai与Ai-1不是互为充要条件,我们无法仅通过上一个状态得到下一个状态,因此可以采用一下方法:

  • {A0->A1}、{A0,A1->A2}、{A0,A1,A2->A3}、...、{A0,A1,A2,A3,...,Ai-1->Ai}。

在这个方法中,下一个状态需要用到前面所有状态才完成,这个推理过程就是“动态规划法”。

能用动态规划解决的问题需具备的特点

(1)最优化原理:如果问题的最优解包含的子问题的解也是最优解,称其有最优子结构,满足最优解原理。

(2)无后效性:某个子问题一旦确定,将不受后面问题状态的影响,只与之前状态有关。 (3)有重叠子问题:子问题间不是独立的,一个问题在后面的决策中可能被多次用到(动态规划区别与其他算法的优势所在)。

动态规划一般思路

动态规划解决的是多阶段决策问题,通常由初始状态开始,经过中间决策序列过程,到达结束状态,确定完成整个过程的一条活动路线(通常是最优解路线)。

初始状态-> 决策1 ->决策2 ->决策3 -> ... -> 决策n -> 结束状态

其设计模式一般为以下几个步骤:

(1)划分阶段:按照问题的一定特征划分为若干有序阶段或子问题。

(2)确定状态和状态变量:将各个阶段的问题的客观情况用不同状态表示出来,同时,要确保状态无后效性。

(3)确定决策并写出状态转移方程:状态转移是根据上一阶段的决策和状态来推导的本阶段的状态。确定了决策,状态转移方程便可写出,通常根据相邻两个阶段的状态间关系来确定决策方法和状态转移方程。

(4)寻找边界条件:用于结束递推的条件。

算法实现

使用动态规划解决问题,最重要的是动态规划三要素:(1)问题的阶段(2)每个阶段的状态(3)从前一个状态到下一个状态之间的递推关系。

算法实现步骤

1.创建一维或二维数组,存放每个子问题结果。

2.设置数组边界值,通常在首位设置边界值。

3.找到每一个状态跟其上一个状态的关系,写出状态转移方程,根据状态转移方程写出代码。

4.返回需要的结果,通常为数组最后一个。

通过几个例子,帮助理解动态规划。

1、斐波那契数列

public static int solutionFibonacci(int n){
    if(!n)return 0;
    else if(n==1)return 1;
    int result[n+1];    //存放子问题结果的数组
    result[0] = 0; result[1] = 1;  //确定规模为0的结果
    for(int i=2;i<=n;i++)
        result[i] = result[i-1] + result[i-2];    //状态转移方程
    return result;   //返回结果
    
}

2、跳台阶

每次只能跳1层或2层,n层台阶有多少种跳法。

第一层台阶跳法为f(1),第二层台阶跳法为f(2)=f(1)+1,第三层台阶跳法为f(3)=f(1)+f(2),第四层台阶跳法f(4)=f(2)+f(3),...,则第n层台阶跳法有f(n)+f(n-1)+f(n-2),与斐波那契数列相似。代码如下:

public static int JumpStairs(int n){
    if(n==1)return 1;
    else if(n==2)return 2;
    int result[n+1];    
    result[1] = 1; result[2] = 2;  
    for(int i=3;i<=n;i++)
        result[i] = result[i-1] + result[i-2];  
    return result[n];   
    
}