zoukankan      html  css  js  c++  java
  • 转【算法之动态规划(一)】动态规划(DP)详解

    一、基本概念

     

    动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。

     动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

    二、基本思想与策略

        基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

        由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

        与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

    区别:

    (1)动态规划和分治区别:

    动态规划算法:它通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。

    分治法:若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。

    注:不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。


    三、适用的情况

    能采用动态规划求解的问题的一般要具有3个性质:

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

        (2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

       (3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)


    四、求解的基本步骤

         动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

        初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

                          图1 动态规划决策过程示意图

        (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

        (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

        (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

        (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

        一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

    实际应用中可以按以下几个简化的步骤进行设计:

        (1)分析最优解的性质,并刻画其结构特征。

        (2)递归的定义最优解。

        (3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

        (4)根据计算最优值时得到的信息,构造问题的最优解


    五、算法实现的说明

        动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

         使用动态规划求解问题,最重要的就是确定动态规划三要素:

        (1)问题的阶段 (2)每个阶段的状态

        (3)从前一个阶段转化到后一个阶段之间的递推关系。

         递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

        确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

              f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}


    六、动态规划算法基本框架

     1 for(j=1; j<=m; j=j+1) // 第一个阶段
     2    xn[j] = 初始值;
     3 
     4  for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段
     5    for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式
     6      xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};
     8 
     9 t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案
    10 
    11 print(x1[j1]);
    12 
    13 for(i=2; i<=n-1; i=i+1)
    15 {  
    17      t = t-xi-1[ji];
    18 
    19      for(j=1; j>=f(i); j=j+1)
    21         if(t=xi[ji])
    23              break;
    25 }
     

    例 最短路径问题

    如图,给定一个运输网络,两点之间连线上的数字表示两点间的距离。试求一条从A到E的运输路线,使总距离最短。

    从图中可以看出,我们可以把从A到E的过程分成若干个阶段,这里是四个阶段。处于每个阶段时,都要选择走哪条支路——决策,一个阶段的决策除了影响该阶段的效果之外,还影响到下一阶段的初始状态,从而也就影响到整个过程以后的进程。因此,在进行某一阶段的决策时,就不能只从这一阶段本身考虑,而应使整体的效果最优。

    我们可以从最后一个阶段开始,由终点向始点方向逐阶递推,寻找各点到终点的最短路径,当递推到始点时,即得到了从始点到终点的全过程最短路。这种由后向前的递推方法,正是动态规划的寻优思想。

     下面我们对这个问题进行求解。把从A到E的全过程分为四个阶段,用k表示阶段变量。第一阶段,有一个初始状态A,三条可供选择的支路,以此类推。我们用)表示在第k阶段由初始状态到下阶段的初始状态的支路距离。用)表示从第k阶段的到终点E的最短距离。

    用逆序递推的方法:

    1.阶段k = 4

    第4阶段有两个初始状态。若全过程最短路径经过,则有)= 4 ;若全过程最短路径经过,则有)= 3 。

    2.阶段 k = 3

    假设全过程最短路径在第3阶段经过点:

    若由,则有)+)=  4 + 4 = 8

    若由,则有)+)=  6 + 3 = 9

    因此,)= min(8,9)= 8 ,即由的最短路径是,最短距离是8。

    类似地,假设全过程最短路径经过点,则有

    )= min{[)+ )],[)+ )]}

             = min (7,8) = 7

    即由的最短路径是由,最短距离是7。

    同理可得出:)= min ( 6 , 6 ) = 6

    的最短路径有两条,其距离都是6。

    3.阶段 k = 2

    类似地,可计算如下:

     =  min( 15 , 14 , 14 ) = 14

     =  min(11, 12 , 12 ) = 11

     =  min(14 , 15 , 13 ) = 13

    因此,由的最短路径有三条,最短距离都是14;由的最短路径是,距离是11;由的最短路径有两条:,距离是13。

    4.阶段 k = 1

     = min ( 16 , 15 , 16 ) = 15

    因此,由的全过程最短路径是,最短距离是15。

    从以上过程可以看出,每个阶段中,都求出本阶段的各个初始状态到终点E的最短路径和最短距离,当逆序递推到过程始点A时,便得到全过程的最短路径及其最短距离,同时得到一族最优结果(即各阶段的各状态到终点E的最优结果)。和穷举法相比,逆叙递推方法大大减少了计算量,且大大丰富了计算结果。

    此题也可以用顺序递推的方法求解,解法过程相似,在此就不赘述了。

    例:求子数组之和的最大值

    一个有N个元素的一维数组(a[0], a[1]….a[n-1]),我们定义连续的a[i] ~ a[j],0<= i, j <=n-1为子数组。

    显然这个数组中包含很多子数组,请求最大的子数组之和。

    如果不想时间复杂度,用遍历所有可能子数组,然后找出最大值就可以了。

    现在如果要求时间复杂度最小,那么肯定是要DP解的。

    我们假设定义两个数组:

    all[i]:表示从i~n-1,最大的子数组之和。

    start[i]:表示包含i,并且从i~n-1,最大子数组之和。

    all[i]中max只有三种可能:

    (1) a[i]单独就是最大,之后再加一个就会变小。
    (2)a[i]+…a[j]最大,即start[i]最大
    (3)a[x]+..a[j]最大,即不包含i的后序某一个子数组和最大。

    最终,最大的子数组之和是all[0]。根据上述3个可能,很容易写出如下递推式:

    start[i] = max (a[i], a[i]+start[i+1])
    all[i] = max(start[i], all[i+1])

    注意我们把上面max(a, b, c)拆成了两个max(a, b)

    由于我们在计算start[i]/all[i]时候需要start[i+?]的值,所以我们从后向前递推dp。

    代码如下,时间复杂度O(n):

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
     
    int max(int a, int b)
    {
        if(a>b)
        {
            return a;
        }else
        {
            return b;
        }
    }
     
    int max_sum(int* arr, int n)
    {
        // Helper array
        int i;
        int* start = (int*)malloc(sizeof(int)*n);
        int* all = (int*)malloc(sizeof(int)*n);
        int final;
        if(!start || !all)
        {
            return -1;
        }
        memset(start, 0, sizeof(int)*n);
        memset(all, 0, sizeof(int)*n);
        // dp
        start[n-1] = arr[n-1];
        all[n-1] = arr[n-1];
        for(i=n-2;i>=0;i--)
        {
            start[i] = max(arr[i], arr[i]+start[i+1]);
            all[i] = max(start[i], all[i+1]);
        }
        final = all[0];
        // Free helper array
        free(start);
        free(all);
        return final;
    }
     
    int main()
    {
        //int arr[6] = {1, -2, 3, 5, -3, 2}; // 8
        int arr[6] = {0, -2, 3, 5, -1, 2}; // 9
        //int arr[5] = {-9, -2, -3, -5, -3}; // -2
        printf("max sum of sub_arr: %d ", max_sum(arr, sizeof(arr)/sizeof(int)));
        return 0;
    }
     

    来源:http://blog.csdn.net/cangchen/article/details/45044811

  • 相关阅读:
    C++中使用多线程
    hdu 4223 dp 求连续子序列的和的绝对值最小值
    hdu 1372 bfs 计算起点到终点的距离
    hdu 4217 线段树 依次取第几个最小值,求其sum
    心得
    hdu 1175 bfs 按要求进行搜索,是否能到达,抵消两个(相同)棋子
    hdu 4221 greed 注意范围 工作延期,使整个工作时间罚时最少的单个罚时最长的值
    hdu 2844 多重背包 多种硬币,每一种硬币有一点数量,看他能组成多少种钱
    uva LCDDisplay
    hdu 4218 模拟 根据一个圆点和半径画一个圆 注意半径要求
  • 原文地址:https://www.cnblogs.com/gispf/p/6733036.html
Copyright © 2011-2022 走看看