动态规划(dynamic programming,dp)入门详解(一)

动态规划(dynamic programming,dp)入门详解(一)

今天我们开始学习算法中非常重要的一项,动态规划。首先我们先来看下动态规划是什么。

什么的动态规划

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
举例:
线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;
区域动规:石子合并, 加分二叉树,统计单词个数,炮兵布阵等;
树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;
背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶(同济ACM第1132题)等;
应用实例:
最短路径问题 ,项目管理,网络流优化等;
该解释来自于百度百科

百度百科主要给出了dynamic programming的一个大致的介绍,和丰富的应用,但是对它的实质讲的很模糊,下面我们来详细看下,dp算法到底是什么:
首先,动态规划算法与分治法类似,它的基本思路是将待求解问题分解成若干子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,适用于动态规划求解的子问题数目太多,以致最后解决原问题需要耗费指数级时间。然而,不同子问题的数目常常只有多项式量级。在用分治算法求解时,有些子问题被重复计算多次。如果能保存已解的子问题的答案,则可以避免重复的计算。为了达到这个目的,可以用一个表来记录已解决的子问题的答案。不管该子问题以后是否会用到,只要计算过,就将其结果填入表中,后面如果要用,就直接取出它的值,这就是动态规划的基本思想。

简而言之,就是在求解一个问题的时候,先求解它的若干的子问题,在求解它的子问题的时候,先求解它的子问题的子问题…………,同时,我们在求解子问题的时候,将子问题的结果记录下来,来减少重复的计算。

动态规划的解题步骤

动态规划算法通常可以按下面的顺序进行解题:

  1. 找出最优解的性质,并找出它的结构特性
  2. 递归的定义最优值
  3. 以自底向上的方式计算最优值
  4. 根据计算最优值时得到的信息,构造最优解

动态规划算法使用示例(1)

下面我们通过例子来讲解一下动态规划算法的使用。

首先,我们以大家熟悉的fibonacci数列来进行演示。
fibonacci数列的定义大家都很熟悉,第一项,第二项为1,后面的每一项为前两项之和。
递归定义为:fibonacci(n) = fibonacci(n-1) + fobonacci(n-2) (n>2)
fibonacci(n) =1 (n<=2)

看着fibonacci数列的定义,我们很容易的写出递归方式的求解第n项的值:

#include <iostream>
using namespace std;
int fibonacci(int n){
    if(n <= 2)
        return 1;
    else
        return fibonacci(n-1) + fibonacci(n-2);
}
int main(){
    int n;
    cin>>n;
    cout<<fibonacci(n);
    return 0;
}

如果使用递归的方法进行求解的话,他会进行很多重复的计算,例如我们计算fibonacci(5)的时候,他会计算fibonacci(4)和fibonacci(3) ,当我们计算fibonacci(4)的时候,会计算fibonacci(3) 和 fibonacci(2) 。这样我们就重复计算机了fibonacci(3),数据量小的时候可能还没什么问题,当数据量大的时候,会额外占用非常大的时间和空间,这肯定是不行的。

下面我们来思考,我们要计算fibonacci(n)(n>2)的话,我们要计算fibonacci(n-1)和fibonacci(n-2) 因此,我们只要知道了fibonacci(n-1)和fibonacci(n-2),就可以得出fibonacci(n)的值。因此,他的状态转移方程为:
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2) (n>2)
我们可以记录每个计算过的值,后面使用到它时,直接取出它的值,来减少重复的计算。

#include <iostream>
using namespace std;
int fib[10000005]={0};
int fibonacci(int n){
    fib[n] = fib[n-1] + fib[n-2];
}
int main(){
    int n;
    cin>>n;
    if (n <= 2){
        cout<<"1";
        return 0;
    }
    fib[1] = 1;
    fib[2] = 1;
    for(int i = 3;i <= n; i++){
        fibonacci(i);
    }
    cout<<fib[n];
    return 0;
}

然后我们在看一下他们的时间效率。
我们分别运行上面两端程序,将n设为50.
我们运行第一个递归写法的时候,运行时间为:45279.2ms
我们运行第一个动态规划写法的时候,运行时间为:0.026ms
当求第50项时,递归算法的所使用的的时间是动态规划算法所用时间的1741507.69倍,我们摸个零,1700万倍。这是个极其夸张,恐怖的数字,这也是dp的魅力所在。

我本来是打算跑100万项的,结果递归写法跑了10几分钟出不来结果,动规写法跑了4.808ms。
后面算了下,递归写法是O(N!)的时间效率,如果跑100万项的话,我这个电脑,怕是得跑上一天都不一定跑完,这时候就是几十亿倍的时间差了。

动态规划算法使用示例(2)

下面我们在通过一个OJ题目来看下蓝桥杯的求解过程。这是一题蓝桥杯的题目,2017年第九届蓝桥杯的第四题。
题目为:
标题:测试次数

x星球的居民脾气不太好,但好在他们生气的时候唯一的异常举动是:摔手机。
各大厂商也就纷纷推出各种耐摔型手机。x星球的质监局规定了手机必须经过耐摔测试,并且评定出一个耐摔指数来,之后才允许上市流通。

x星球有很多高耸入云的高塔,刚好可以用来做耐摔测试。塔的每一层高度都是一样的,与地球上稍有不同的是,他们的第一层不是地面,而是相当于我们的2楼。

如果手机从第7层扔下去没摔坏,但第8层摔坏了,则手机耐摔指数=7。
特别地,如果手机从第1层扔下去就坏了,则耐摔指数=0。
如果到了塔的最高层第n层扔没摔坏,则耐摔指数=n

为了减少测试次数,从每个厂家抽样3部手机参加测试。

某次测试的塔高为1000层,如果我们总是采用最佳策略,在最坏的运气下最多需要测试多少次才能确定手机的耐摔指数呢?


首先我们拿到这题的时候,要先分析题目的内容,因为手机数量是有限的,所以我们不能直接使用二分法,因为当手机没了之后,我们就没法进行下面的测试,无法得出最后的耐摔指数,下面我们来分析如何来求得最坏情况下的最少测试次数。

我们设当前有phone_num个手机,level_num层,我们使用test ( phone_num, level_num )来表示最坏情况下的最少测试次数

当我们有一个手机的时候,不论有多少层,我们只能从第一层开始一层一层的摔,因为如果隔层或者不从第一层开始,他一旦摔碎了就没有手机进行下面的测试了,就无法得出结果了。

当我们有phone_num(phone_num > 1)的时候,我们就可以不从第一层开始一层层的摔了,我们第一次可以摔第 i 层,这个时候我们又有两种情况,就是摔碎了和没摔碎:

第一种情况,没摔碎。那我们这种情况下,我们需要测试的楼层就减少了 i 层,手机的数量不变, 即为 1 + test ( phone_num, level_num - i ),别忘了前面加上这次摔的次数1.

第二种情况,摔碎了。那我们这种情况下,我们需要测试的楼层就是 i - 1 层,手机数列减1,即为 1 + test ( phone_num - 1, i  - 1)
这时候我们就可以总结出dp算法中最关键的一步,状态转移方程。

test ( phone_num, level_num ) = min( ( 1 + test ( phone_num, level_num - 1) ), max ( (1 + test( phone_num, level_num - i  ) ), ( 1 + test( phone_num -1, i -1 ) ) )

这样写可能不太好看,我们再简化一下这个式子:把 1 提出来。

test ( phone_num, level_num ) = min( test( phone_num, level_num -1 ), max( test(phone_num, level_num - i), test ( phone_num - 1, i -1 ) ) ) + 1

#include <iostream>
using namespace std;
int dp[10005][11]={0};
int main(){
    int phone_num, level_num;
    cin>>level_num>>phone_num;
//    level_num = 1000;
//    phone_num = 3;
    for(int i = 1; i <= level_num; i++)
        dp[i][1] = i;
    for(int i = 2; i <= phone_num; i++){
        for(int j = 1; j <= level_num; j++){
            dp[j][i] = 1 + dp[j-1][i];//先赋值一个可能的最大值,即从上一层的次数 +1
            for(int k = 2;k < j ;k++){//第一次摔一层的,上面已经赋值了,所以我们从第一次摔第二层开始
                dp[j][i] = min(dp[j][i], 1 + max(dp[j - k][i],dp[k - 1][i -1]) );
            }
        }
    }
    cout<<dp[level_num][phone_num];
    return 0;
}
发布了97 篇原创文章 · 获赞 50 · 访问量 8180

猜你喜欢

转载自blog.csdn.net/qq_43422111/article/details/103059559