当前位置 : 主页 > 编程语言 > java >

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

来源:互联网 收集:自由互联 发布时间:2022-10-26
动态规划(dynamic programming,dp)入门详解(一) 今天我们开始学习算法中非常重要的一项,动态规划。首先我们先来看下动态规划是什么。 什么的动态规划 动态规划(dynamic programming)是运


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

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

什么的动态规划

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

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

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

动态规划的解题步骤

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

  • 找出最优解的性质,并找出它的结构特性
  • 递归的定义最优值
  • 以自底向上的方式计算最优值
  • 根据计算最优值时得到的信息,构造最优解
  • 动态规划算法使用示例(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;
    }


    上一篇:PAT 乙级 1060 爱丁顿数 (25分)
    下一篇:没有了
    网友评论