算法学习(3):动态规划DP

来源:互联网 发布:好玩的网络手游 编辑:程序博客网 时间:2024/06/06 12:43
  • 动态规划过程:

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

  • 基本思想:

 将待求解的问题分解为若干个阶段,按顺序求解子阶段,前一子阶段的解,为后一子阶段的求解提供了有用的信息,存在一定的递推关系。即可以由(n1)的最优推导出n的最优。

  • 基本做法

  1.划分阶段,寻找从前一个阶段转化到后一个阶段之间的递推关系

  2.把子阶段信息保存在一个数组中(一维,二维或 三维)

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

  • 什么时候用动态规划

拿到一个问题时首先可以对其进行分类。对于可以拆分成子问题的,如果子问题之间相互独立,用分治法,如果子问题的前一个阶段和后一个阶段有递推关系,则适合用动态规划
  • 动态规划应用1--01背包问题

    有N件物品和一个容量为V的背包,第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

    解析:
    这是最基础的背包问题,特点是每种物品仅有一件,可以选择放或不放。用子问题定义状态,即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。其状态转移方程便是f[i][v] = max{f[i-1][v], f[i-1][v-c[i]]+w[i]},这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的,所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只关系前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,其价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

    算法实现:

    #include<stdio.h>  #include<string.h>  #define N 1000  int f[N][N];    int main()  {      int w[N], c[N], i, j, n, v;      // 输入物品的个数;输入背包的体积      scanf_s("%d%d", &n, &v);      // 输入n种物品的价值;      for(i = 1; i <= n; i++)          scanf_s("%d", &w[i]);      // 输入n种物品的体积      for(i = 1; i <= n; i++)          scanf_s("%d", &c[i]);        // 初始化数组f的元素为0      memset(f, 0, sizeof(f));        for(i = 1; i <= n; i++)          for(j = 0; j <= v; j++)          {              // 如果当前物品的体积小于背包体积,              // 且当前物品的价值加上背包剩下的空间能放下的物品的价值大于上一次选择的最佳方案,则更新f[i][j]              if(c[i] <= j && f[i-1][j] < (f[i-1][j-c[i]] + w[i]))                  f[i][j] = f[i-1][j-c[i]] + w[i];              else                  f[i][j] = f[i-1][j];          }      printf("%d\n", f[n][v]);        return 0;  }  
  • 动态规划应用1--数塔问题

    有N件物品和一个容量为V的背包,第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

    给定一个数塔,其存储形式为如下所示的下三角矩阵。在此数塔中,从顶部出发,在每一节点可以选择向左走还是向右走,一直走到底层。请找出一条路径,使路径上的数值和最大。
    输入样例: 


    12   15  

    10   6     8 

    2     18   9     5 

    19   7     10   4   16

    输出样例:

    max = 59
    9->12->10->18->10


    阶段划分:

    从数塔问题的特点来看,不难发现解决问题的阶段划分,应该是自下而上逐层决策。不同于贪心策略的是做出的不是唯一的决策,第一步对于第五层的8个数据,做如下4次决策:

    • 对经过第四层2的路径,在第五层的19,7中选择19;
    • 对经过第四层18的路径,在第五层的7,10中选择10;
    • 对经过第四层9的路径,在第五层的10,4中也选择10;
    • 对经过第四层5的路径,在第五层的4,16中选择16。

    这是一次决策过程,也是一次递推过程和降阶过程。因为以上的决策结果将5阶数塔问题变为4阶子问题,递推出第四层与第五层的和为:

    21(2 + 19), 28(18 + 10), 19(9 + 10), 21(5 + 16)

    用同样的方法还可以将4阶数塔问题,变为3阶数塔问题,...,最后得到的1阶数塔问题,就是整个问题的最优解。

    存储求解:

    1)原始信息存储

    原始信息有层数和数塔中的数据,层数用一个整型变量n存储,数塔中的数据用二维数组data,存储成如下的下三角阵:


    12   15  

    10   6     8 

    2     18   9     5 

    19   7     10   4   16

    2)动态规划过程存储

    由于早期阶段动态规划决策的结果是一组数据,且这次的决策结果是下次决策的唯一依据(无后效性),所以必须再存储每一次决策的结果,若仅仅是求最优解,用一个一维数组存储最新的决策结果即可;但若要同时找出最优解的构成或路径,则必须用二维数组d存储各阶段的决策结果。根据上面的算法设计,二维数组d的存储内容如下:

    d[n][j] = data[n][j], j = 1, 2, ..., n;

    当i = n-1, n-2, ..., 1, j = 1, 2, ..., i时:

    d[i][j] = max(d[i+1][j], d[i+1][j+1]) + data[i][j]

    最后d[1][1]存储的就是问题的结果。

    数塔及动态规划过程数据,如下所示:

    数组data                             数组d                                      

    9                                         59

    12   15                                50   49    

    10   6     8                           38   34   29

    2     18   9     5                    21   28   19   21

    19   7     10   4   16             19   7     10   4    16

    3)最优解路径求解及存储

    通过数组data和数组d可以找到最优解的路径,但需要自顶向下比较数组data和数组d中的数据。

    输出data[1][1]“9”。

    b = d[1][1] - data[1][1] = 59 - 9 = 50,b与d[2][1],d[2][2]比较,b与d[2][1]相等,输出data[2][1]“12”。

    b = d[2][1] - data[2][1] = 50 - 12 = 38,b与d[3][1],d[3][2]比较,b与d[3][1]相等,输出data[3][1]“10”。

    b = d[3][1] - data[3][1] = 38 - 10,b与d[4][1],d[4][2]比较,b与d[4][2]相等,输出data[4][2]“18”。

    b = d[4][2] - data[4][2] = 28 - 18,b与d[5][2],d[5][3]比较,b与d[5][3]相等,输出data[5][3]“10”。

    说明:

    据此,可以写出根据数组data和数组d求解最优解路径的算法。

    算法实现:

    为了提高算法的时间效率,还可以在动态规划的过程中,同时记录每一步决策选择数据的方向,这又需要一个二维数组。为了设计简洁的算法,最后用三维数组a[50][50][3]存储以上确定的3个数组的信息。a[50][50][1]代替数组data,a[50][50][2]代替数组d,a[50][50][3]记录解路径。

    其中,a[50][50][3] = 0表示向下(在塔中是向左)“走”,如第三层2与下面的19求和;其中,a[50][50][3] = 1表示向右“走”,如第三层18与下面的10求和

    #include<iostream>  using namespace std;    int main()  {      int a[50][50][3], i, j, n;      cout << "please input the number of rows: ";      cin >> n;      for(i = 1; i <= n; i++)          for(j = 1; j <= i; j++)          {              cin >> a[i][j][1];              a[i][j][2] = a[i][j][1];              a[i][j][3] = 0;          }          for(i = n - 1; i >= 1; i--)              for(j = 1; j <= i; j++)                  if(a[i+1][j][2] > a[i+1][j+1][2])                  {                      a[i][j][2] = a[i][j][2] + a[i+1][j][2];                      a[i][j][3] = 0;                  }                  else                  {                      a[i][j][2] = a[i][j][2] + a[i+1][j+1][2];                      a[i][j][3] = 1;                  }                  cout << "max = " << a[1][1][2] << endl;                  j = 1;                    for(i = 1; i <= n-1; i++)                  {                      cout << a[i][j][1] << "->";                      j = j + a[i][j][3];                  }                  cout << a[n][j][1] << endl;                  return 0;  }