首页 > 代码库 > 关于动态规划的理解

关于动态规划的理解

动态规划是个比较有趣的算法,第一次接触动态规划也是从一个比较特别的教程开始的,这里贴出原文地址http://blog.csdn.net/woshioosm/article/details/7438834
看完原文回到这里,其实我觉得很多像我这种C语言刚刚入门的人,只理解到了动态规划的转移方程,以原文中的例子为例:
当mineNum = 0且people >= peopleNeeded[mineNum]时 f(people,mineNum) = gold[mineNum]
当mineNum = 0且people < peopleNeeded[mineNum]时 f(people,mineNum) = 0
当mineNum != 0时 f(people,mineNum) = f(people-peopleNeeded[mineNum], mineNum-1) + gold[mineNum]与f(people, mineNum-1)中的较大者
其中,前两个式子对应动态规划的“边界”,后一个式子对应动态规划的“最优子结构”。
菜鸟的我觉得这个比较容易实现啊,于是有了下面的代码:
 1 int goldGet(int People, int mineNum) 2 { 3      if(mineNum == 0) 4      { 5           /*考虑最后一座金矿是,如果剩余开采金矿的总人数 6            多余或者等于需要的人数,则返回该金矿的金子数 7            目,否则返回0。*/ 8           if(People >= PeopleNeeded[mineNum]) 9           {10                return Gold[mineNum];11           }12           else13           {14                return 0;15           }16      }17      else18      {19           int m, n;20           /*考虑第mineNum座金矿,第一种情况:开采该座金矿所能获得的最多的金子数目,21            为该座金矿的金子数目和剩下的人开采剩下的金矿所得到的最多的金子数目的和22            第二种情况:部开采该座金矿所获得的最多的金子数目,是目前所有的人用来开23            采剩余的金矿获得的最多的金子数目。返回两种情况下金子数目较多的一种情况24            的金子数目。*/25           if(People >= PeopleNeeded[mineNum])26           {27                m = Gold[mineNum] + goldGet(People - PeopleNeeded[mineNum], mineNum - 1);28           }29           else30           {31                m = goldGet(People, mineNum - 1);32           }33           n = goldGet(People, mineNum - 1);34 35           return m >= n ? m : n;36      }37 }
后面的关于时间复杂度和空间复杂度的优化对我像我这种境界的人来说,这个理解有点困难,我就暂且自我安慰一下,告诉自己已经理解了动态规划,并且把文章最后的金矿问题做了,并且通过了测试数据,全部代码如下:
#include <stdio.h>#include <stdlib.h>#define MaxNum 500int PeopleNeeded[MaxNum]; //PeopleNeeded[i]表示第i座金矿需要的开采人数int Gold[MaxNum]; //Gold[i]表示第i座金矿开采后得到的金子数目int goldGet(int People, int mineNum){     if(mineNum == 0)     {          /*考虑最后一座金矿是,如果剩余开采金矿的总人数           多余或者等于需要的人数,则返回该金矿的金子数           目,否则返回0。*/          if(People >= PeopleNeeded[mineNum])          {               return Gold[mineNum];          }          else          {               return 0;          }     }     else     {          int m, n;          /*考虑第mineNum座金矿,第一种情况:开采该座金矿所能获得的最多的金子数目,           为该座金矿的金子数目和剩下的人开采剩下的金矿所得到的最多的金子数目的和           第二种情况:部开采该座金矿所获得的最多的金子数目,是目前所有的人用来开           采剩余的金矿获得的最多的金子数目。返回两种情况下金子数目较多的一种情况           的金子数目。*/          if(People >= PeopleNeeded[mineNum])          {               m = Gold[mineNum] + goldGet(People - PeopleNeeded[mineNum], mineNum - 1);          }          else          {               m = goldGet(People, mineNum - 1);          }          n = goldGet(People, mineNum - 1);          return m >= n ? m : n;     }}int main(){     int People; //开采金矿的总人数     int GoldNum; //金矿数目     int GoldGet; //最终能够获取的金子的总数     int i, j; //用来进行的便利的变量     scanf("%d %d", &GoldNum, &People);     for(i = 0; i < GoldNum; i ++)     {     scanf("%d %d", &PeopleNeeded[i], &Gold[i]);     }     GoldGet = goldGet(People, GoldNum);     printf("%d\n", GoldGet);     return 0;}
当然事情还没有结束,因为我觉得我理解了动态规划,于是跑到hihocoder上跃跃欲试(附hihocoder地址:http://hihocoder.com/,一个很不错的OJ),菜鸟的我只敢尝试01背包那种简单的题目。把代码稍微修改了一下,符合了题目的输入和输出,很兴奋的提交了,结果"Time Limit Exceeded"。很郁闷,看来我还是没有真正的理解动态规划,还需要把优化的那部分看懂,还好hihocoder上每道题都会对用到的算法进行讲解,这回稍微弄懂了,修改了代码提交,然后AC了,这里先给出代码:
/*01背包问题*/#include <stdio.h>#include <stdlib.h>#include <memory.h>int max(int x, int y){     return x >=y ? x : y;}int main(){     int N; //表示奖品的个数     int M; //表示奖券数          int need[500]; //need[i]表示第i个奖品需要的奖券数     int value[500]; //value[i]表示第i个奖品的评分值              int best[100000]; //best[j]表示对于i个奖品时j张奖券的最大评分值     memset(best, 0, sizeof(best)); //初始化best的所有元素为0     int i, j; //程序进行遍历的变量     scanf("%d %d", &N, &M);     for(i = 0; i < N; i ++)     {          scanf("%d %d", &need[i], &value[i]);     }     for(i = 0; i < N; i ++)     {          for(j = M - 1; j >= need[i]; j --)          {               best[j] = max(best[j], best[j - need[i]] + value[i]);          }     }     printf("%d\n", best[M - 1]);     return 0;}
嗯。。。这里代码貌似要断了很多,对于前面的输入输出大家都看得懂,主要的是最后一个循环,其实这个循环也比较好理解,在我贴的第一个比较low的程序里面,转移方程是用递归实现的,这里改成了双重循环,其实是一个意思。最难理解的是:
best[j] = max(best[j], best[j - need[i]] + value[i]);
为了理解这句话需要一步步的来,将一开始比较low的递归搞成双重循环(这里不是说递归比较low,而是说我写的比较low)
for(i = N- 1; i > 0; i --){     for(j = M - 1; j >= need[i]; j --)     {          best[i, j] = max(best[i - 1][ j], best[i - 1][j - need[i]] + value[i]);     }}
这里i > 0是因为i = 0没有讨论的必要,其实严格的来说这个循环和前面的递归并不完全一样,这里的时间复杂度更低一些,因为这里的最高收益是用数组进行存储的,对于某次循环中计算出了best[x][y]的值后,后面如果还需要用到就不需要再计算了,而之前的递归用的是函数,并不能存储数值,效率更低。
这里我们可以在复习一下转移方程,
当i != 0时 bes[i][j] 是 best[i - 1][j - need[i]] + value[i]与best[i - 1][j]中的较大者
这里我们可以发现best[Ai][Aj]依赖于best[Bi][Bj]时,肯定有Ai = Bi + 1,以及Aj >= Bj。
举个简单的例子,我们求best[5][j]的时候只与best[4][j]和best[4][j - need[5]]有关,对于前面的best[0][k],best[1][k],...best[3][k],其中k = 0,1,...M-1没有任何关系,那么我们就没有必要用那么复杂的存储。并且j的取值是从M-1到0,考虑到Aj >= Bj的规律,计算best[i][j]的时候,best[i-1][j+1,...M-1]已经没有任何利用价值了,这些空间也是多余的,怎么办呢,其实计算best[i][j - 1]的时候best[i - 1][j]就没有价值了,那么我们计算best[i][j]的结果就可以直接存放到best[i - 1][j]的地址上,这样的话,我们的存储其实只需要一个大小为M的一维数组。并且是best[i][j]覆盖beat[i - 1][j],所以循环的时候i要从0到N-1,
于是循环就可以写成
for(i = 0; i < N; i ++){     for(j = M - 1; j >= need[i]; j --)     {     best[j] = max(best[j], best[j - need[i]] + value[i]);     }}
这个就是我们最后的结果。