动态规划 - 学习笔记(一)

定义

动态规划算法,是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推的方式去处理的一种解决问题的方法。(摘自“百度百科”)

 

 

入门级的动态规划题

十级楼梯,一步上1或者2级,问,有多少种走法走完?”——最简单的动态规划题。

模拟法(其实就是暴力解法)

模拟走楼梯,递归地把走10级楼梯的过程模拟出来,累计就能得出次数了——费时。

----------------------------------------

用动态规划的思维去思考:

走10级楼梯,是一个问题

站在第n级台阶上,是一个状态

走1级或者走2级,退1级或者退2级——是状态转化的操作;是问题的增量单元,放在问题最初就是边界

站在第n级台阶,走1级(或者2级),则到达下一个状态——这是状态递推;

站在第n级台阶,退1级(或者2级),则到达上一个状态——这是状态回退;

用回退的观点看待 “站在第10级楼梯上” 这么一个状态,它的上一个状态必然是站在第9级或者第8级台阶上。那么,走10级楼梯这个大问题,其实就是走8级楼梯走9级楼梯的两个子问题了,因为最后无非都是迈1步。——这是在明确上面几个概念之后,对问题的分解。

用数学表达式表示,设站到第n级台阶的解法是F(n)种,则站到第10级台阶的问题可以通过以下公式拆分:

F(10)  = F(9)+F(8)

     = [F(8)+F(7)]+[F(7)+F(6)]

     = [[F(7)+F(6)]+[F(6)+F(5)]]+[[F(6)+F(5)]+[F(5)+F(4)]]

          = F(1) + ... + F(2)(最终就是由若干个F(1)和F(2)组成)

上面的分解过程就像一棵生长的二叉树,对于树,经典的解法就是递归了。

只要递归地把F(9)和F(8)算出来,就能解决问题了。

但是解法还不能只是简单的递归,因为那样会产生大量重复计算,那样的时间复杂度仍然可观。

有两种办法避免重复计算,这是动态规划问题的两种常见思路

一种是自上而下,递归,在过程中被动建立备忘录(没有了备忘录,这种解法也能解开问题,就是慢而已;备忘录是被动建立的,目的是加速递归)。

把算过的F(x)记录下来,每次递归都应该先查“备忘录”,如果有记录,则省去下面的递归过程了;否则才需要递归地计算)

 1 #include <iostream>
 2 #define N            10
 3 #define INVALID        0
 4 
 5 int reminder[N + 1] = { INVALID, 1, 2 };//1 对F(1)和F(2)赋初值(为了方便表述,第0位空着不用了)
 6 
 7 int F(int n){
 8     if (reminder[n] != INVALID) {
 9         return reminder[n];//3 备忘录查到记录,直接取用
10     }
11     else {
12         return reminder[n] = F(n - 1) + F(n - 2);//2 递归,顺带更新备忘录
13     }
14 }
15 
16 int main(){
17     std::cout << F(10) << std::endl;
18     return 0;
19 }
View Code

一种是自下而上,递推解开所有子问题,主动建立备忘录(备忘录在这种解法里面不可或缺)。

当然是有顺序讲究的:从最小的子问题开始,问题由小到大,递推解开所有子问题,并最终解决F(10)。

 1 #include <iostream>
 2 #define N        10
 3 #define INVALID    0
 4 
 5 int reminder[N] = { INVALID, 1 , 2 };//1 对基础操作(F(1)/F(2))赋初值
 6 
 7 int main(){
 8     //2 递推解决1-10级的问题
 9     for (int i = 3; i <= N; i++) {
10         reminder[i] = reminder[i - 1] + reminder[i - 2];
11     }
12     //3 直接查备忘录,输出结果
13     std::cout << reminder[10] << std::endl;
14     return 0;
15 }
View Code

好啦,这个问题,正确答案是 89 种走法。

入门级的动态规划问题(10级楼梯走法求解)至此解毕。

 


难度加深一点的入门级动态规划题

上面的题目,其实只是一个一元函数的问题:y=f(n)

下面这道题就厉害了,是一个二元函数的问题:y=f(n, w):

5个金矿,挖矿需要的人力如下表:

现有10人,如何分配人力,挖到最多矿藏?

( 限制条件:人力不可重复使用,一人只能对应一矿;矿藏要么不挖要么全挖——“现实中不会这样子的,现在是做题...” )

分析

数学表达

这就是一个二元的题目了,设人力为n,矿藏个数w,挖矿最优解问题可以表述为解一个二元函数 y=f(n,w),具体到这道题就是二元函数求值y=f(10,5)。

问题的分解

最后一个矿不管是哪一个,它都是最后一个,编号5(我可不从0开始的喔),这个矿需要的人力是p(5),矿藏量g(5);

那么剩余分配到剩下4个矿的人力就是10-p(5),就有 y=f(10,5)=f(10-p(5), 4)+g(5);

不过有可能最后一个矿不挖,所有人力集中到前4个矿里面,挖矿量更大喔:y=f(10,5)=f(10,4);

所以综上,y = f(10,5) = max { f( 10-p(5) + g(5) ), f(10,4) }(情况1)

再仔细考虑一下,如果最后一个矿的人力需求p(5)已经超过能提供的人力w,那就没有选择的余地了:y=f(10,5)=f(10,4)(情况2)

注意,直到这里,都没说最后一个矿是哪一个喔...(后面讨论)

递推公式(关键)

就是对上面“问题分解”的抽象,过程省略:

y = f(w,n) = max { f( w-p(n) + g(n) ), f(w, n-1) }(情况1,w>=p(n))

y = f(w,n)=f(w,n-1)(情况2,w<p(n))

边界(关键)

就是w个人挖1个矿(从高等数学角度来看,1个人挖n个矿也是一个边界,不过我们这个问题是人数分配,只剩一个人就没意思了)。

既然只有一个矿,编号肯定就是1啦。该矿人力p(1),矿藏量g(1),挖矿量y=f(1,w)。

既然是边界条件,这个函数自然好解,细心就好:

当w>=p(1)时,y=f(1,w)=g(1);当w<p(1)时,y=f(1,w)=0;

注意,直到这里,都没说边界这一个矿是哪一个喔...(后面讨论)

递推解决问题

接下来就是画表格表述问题的增长过程了。这张表格可以确定就是“人数&矿”组成的,这个没有异议。

人数是增长的,所以由1排到10,这个也没啥异议;

那么,有个问题了,矿要怎么排?

“哪一个矿放在边界情况上,矿藏最多那个,人力要求最多那个?”

“哪一个矿来做最后一个矿,矿藏最多那个,人力要求最多那个?”

事实上,无所谓的,这就是动态规划的魅力所在:我们的动态规划求解基于子问题的解,但我们最终并不关心子问题的解,我们最终只关心手头上这个问题的解。

最初一个矿和最后一个矿是什么无所谓,我们可以确定最后递推解出来的解,是针对5座矿,10个人力的就行了。

上面的话,我自己都明白自己在说什么—。—||,来个数学大师证明一下?

总而言之,人数有增长就行,矿的排序无所谓。所以,不管了,直接动手。

根据边界条件:当w>=p(1)时,y=f(1,w)=g(1);当w<p(1)时,y=f(1,w)=0;下面是我建立的边界初始化完毕的表格:

接下来是处理第二行,根据:

y = f(w,n) = max { f( w-p(n) + g(n) ), f(w, n-1) }(情况1,w>=p(n))

y = f(w,n)=f(w,n-1)(情况2,w<p(n))

1-4个人,属于情况2;5-9个人,属于情况1;10个人也属于情况1,表格进一步完善为:

以此类推,最终表格完善为:

 

过程怎么样不管,我们只想得到答案:人力10人,最大能挖到矿藏900,挖的是500(5)和400(5)两座矿。

事实上,可以试一下,随便打乱矿藏顺序,按照上面的推导过程,可以得到下表:

 过程变化了,但是最终问题不变,答案也不变:人力10人,最大能挖到矿藏900,挖的是500(5)和400(5)两座矿。

 好啦,题目到此解毕,代码如下,很粗糙:

  1 #include <iostream>
  2 typedef struct
  3 {
  4     int worker;
  5     int resource;
  6 }MINE;
  7 typedef struct
  8 {
  9     int  maxMine;
 10     bool dig;
 11 }STRAGECY;
 12 //五座矿
 13 #define  NUM_OF_MINE    5
 14 #define  NUM_OF_WORKER    10
 15 const MINE mines[NUM_OF_MINE] =
 16 {
 17     { 5, 500 },{ 5, 400 },
 18     {3, 350},{4, 300},{3, 200}
 19 };
 20 bool digOrNot[NUM_OF_MINE] = { 0 };
 21 
 22 int getMaxMine(int workers)
 23 {
 24     int maxMine = 0;
 25     STRAGECY maxMineMatrix[NUM_OF_MINE][NUM_OF_WORKER];
 26 
 27     for (int i = 0; i < NUM_OF_MINE; i++)
 28     {
 29         for (int j = 0; j < NUM_OF_WORKER; j++) {
 30             maxMineMatrix[i][j].dig = false;
 31             maxMineMatrix[i][j].maxMine = 0;
 32         }
 33     }
 34     for (int i = 0; i < NUM_OF_MINE; i++)
 35     {
 36         for (int j = 0; j < workers; j++)
 37         {
 38             int worker = j + 1;
 39             //计算:如果挖当前矿,那么剩下人数最多能挖多少矿?
 40             int maxMineIfDig = 0;
 41             if (worker >= mines[i].worker)
 42             {
 43                 maxMineIfDig = mines[i].resource;
 44                 if (worker - mines[i].worker > 0)
 45                 {
 46                     int max = 0;
 47                     for (int k = 0; k < i; k++)
 48                     {
 49                         max = (max > maxMineMatrix[k][worker - mines[i].worker - 1].maxMine) ?
 50                             max : maxMineMatrix[k][worker - mines[i].worker - 1].maxMine;
 51                     }
 52                     maxMineIfDig += max;
 53                 }
 54             }
 55             //计算:如果不挖当前矿,那么所有人数挖前面几个矿能挖最多多少出来?
 56             int maxMineIfNotDig = 0;
 57             for (int k = 0; k < i; k++)
 58             {
 59                 maxMineIfNotDig = (maxMineIfNotDig > maxMineMatrix[k][j].maxMine) ?
 60                     maxMineIfNotDig : maxMineMatrix[k][j].maxMine;
 61             }
 62             //策略比较 和 二维数组更新:
 63             if (maxMineIfDig > maxMineIfNotDig)
 64             {
 65                 maxMineMatrix[i][j].maxMine = maxMineIfDig;
 66                 maxMineMatrix[i][j].dig = true;
 67             }
 68             else
 69             {
 70                 maxMineMatrix[i][j].maxMine = maxMineIfNotDig;
 71                 maxMineMatrix[i][j].dig = false;
 72             }
 73         }
 74     }
 75     //获得人数分配的策略:
 76     int workerLeft = workers;
 77     int mineIndex = NUM_OF_MINE - 1;
 78     memset(digOrNot, 0, NUM_OF_MINE);
 79     while (mineIndex >= 0 && workerLeft>0)
 80     {
 81         if (maxMineMatrix[mineIndex][workerLeft-1].dig)
 82         {
 83             workerLeft -= mines[mineIndex].worker;
 84             digOrNot[mineIndex] = true;
 85         }
 86         mineIndex--;
 87     }
 88     return maxMineMatrix[NUM_OF_MINE-1][workers-1].maxMine;
 89 }
 90 
 91 int main()
 92 {
 93     int maxMine = 0, worker = 0;
 94     while (1) {
 95         scanf("%d", &worker);
 96         maxMine = getMaxMine(worker);
 97         std::cout << "max mine: " << maxMine << std::endl;
 98         for (int i = 0; i < NUM_OF_MINE; i++)
 99         {
100             std::cout << digOrNot[i] << " ";
101         }
102         std::cout<<std::endl;
103     }
104     return 0;
105 }

 

(题目参考了 https://www.sohu.com/a/153858619_466939 )

 

转载于:https://www.cnblogs.com/i-am-normal/p/9949548.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值