zoukankan      html  css  js  c++  java
  • 动态规划分析总结——如何设计和实现动态规划算法

    http://blog.csdn.net/trochiluses/article/details/37966729

    进行算法设计的时候,时常有这样的体会:如果已经知道一道题目可以用动态规划求解,那么很容易找到相应的动态规划算法并实现;动态规划算法的难度不在于实现,而在于分析和设计—— 首先你得知道这道题目需要用动态规划来求解。本文,我们主要在分析动态规划在算法分析设计和实现中的应用,讲解动态规划的原理、设计和实现。在很多情况下,可能我们能直观地想到动态规划的算法;但是有些情况下动态规划算法却比较隐蔽,难以发现。本文,主要为你解答这个最大的疑惑:什么类型的问题可以使用动态规划算法?应该如何设计动态规划算法?

                                                                                 动态规划第一讲——缓存与动态规划

    一、缓存与动态规划

    例一:有一段楼梯有10级台阶,规定每一步只能跨一级或两级,要登上第10级台阶有几种不同的走法?

    分析:很显然,这道题的对应的数学表达式是F(n)=F(n-1) + F(n-2);其中F(1)=1, F(2)=2。很自然的状况是,采用递归函数来求解:

    1. int  solution(int n){  
    2.     if(n>0 && n<=2) return n;  
    3.     return solution(n-1) + solution(n-2);  
    4. }  
    
    
    
    

        如果我们计算F(10), 先需要计算F(9) F(8); 但是我们计算F(9)的时候,又需要计算F(8),很明显,F(8)被计算了多次,存在重复计算;同理F(3)被重复计算的次数就更多了。算法分析与设计的核心在于 根据题目特点,减少重复计算。  在不改变算法结构的情况下,我们可以做如下改进:

    1. int dp[11];  
    2. int  solution(int n){  
    3.     if(n>0 && n<=2) return n;  
    4.     if(dp[n]!=0) return dp[n];  
    5.     dp[n] = solution(n-1) + solution(n-2);  
    6.     return  dp[n];  
    7. }  

    这是一种递归形似的写法,进一步,我们可以将递归去掉:

    1. int  solution(int n){  
    2.     int dp[n+1];  
    3.     dp[1]=1;dp[2]=2;  
    4.     for (i = 3; i <= n; ++i){  
    5.         dp[n] = dp[n-1] + dp[n-2];  
    6.     }  
    7.     return  dp[n];  
    8. }  

    当然,我们还可以进一步精简,仅仅用两个变量来保存前两次的计算结果; 这个算法留待读者自己去实现

    例二:01背包问题

    有n个重量和价值分别为vector<int> weight, vector<int> value的物品;背包最大负重为W,求能用背包装下的物品的最大价值?

    输入:n =4 
    weight=2, 1, 3, 2
    value =3, 2, 4, 2
    W=5
    输出=7


    思考一:我们可以采用穷举法,列出n个物品的所有组合形式,从中选取符合条件的最大价值:

    采用穷举法,必然需要能够举出所有状态,不重不漏;而如何穷举,方法多种多样,我们的任务是要穷举有n个元素组成的所有子集。而穷举的方法主要有两种—— 递增式(举出1~100之内的所有数字, 从1到100);和分治式的穷举(例如举出n个元素的集合,包含两种—— 含有元素a和不含元素a的)。于是,我们基于穷举法得到背包问题的第一种算法—— 递归与分治。

    1. int rec(int i, int j){//从i到n号物品,选择重量不大于j的物品的最大价值  
    2.     int res;  
    3.     if(i==n){  
    4.         res=0;  
    5.     }   
    6.     else if(j< w[i]){  
    7.         res = rec(i+1, j);  
    8.     }  
    9.     else{  
    10.         res = max(rec(i+1, j), rec(i+1, j-w[i])+v[i]);  
    11.     }  
    12.     return res;  
    13. }  

    调用res(0, W), 即可得到结果. 时间复杂度O(2^n);我们来分析一下递归调用的情况。

    为了偷懒,最后一行没有画出来,但是注意红色的部分,我们会发现(3, 2)这个子问题被计算了两次,很显然,如果问题规模足够大,数据足够多样,这种重复计算导致的时间耗费将更多。


    改进:采用递归加缓存的策略
    此时,时间复杂度是O(nW); 代码就省略不写了。


    思考二:上文中的记忆化搜索,如果可以将递归变为循环,这就是动态规划,对应的数学表达式如下:

    1. dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]] + v[i]);//对应的计算表格如下和程序如下:  
    2. void solution(){  
    3.     fill(dp[n], dp[n]+W, 0);  
    4.     for (int i = n-1; i >= 0; --i){  
    5.         for (j = 0; j <= W; ++j){  
    6.             if(j < w[i]) dp[i][j] = dp[i+1][j];  
    7.             else dp[i][j] = max(dp[i+1][j], dp[i+!][j-w[i]]+v[i]);  
    8.         }  
    9.     }  
    10.     return dp[0][W];  
    11. }  

    思考三:递归形式的多样化

    我们刚才的递归计算,在i这个维度是逆向的,同样我们可以采用正向的DP。规定dp[i][j]表示前i号物品中能选出重量在j之内的最大价值,则有递推式
    dp[i][j] = max(dp[i-1][j] , dp[i-1][j-w[i]] + v[i]);

    思考四:我们是如何想到递归算法的?

    也许,DP算法的难度不在于告诉你这个题目需要用DP求解,然后让你来实现算法。而在于你首先得意识到这道题目需要用递归求解,这里我们通过分析上面的思考步骤来总结DP算法的典型特征:
    1>DP算法起源于DC—— 一个问题的解,可以先分解为求解一系列子问题的解,同时包含重叠子问题:于是,我们得到DP算法的第一个黄金准则某个问题具有独立而重叠的子问题;子问题不独立,没法进行分治;子问题不重叠,没有进行DP的必要,直接用普通的分治法就可以了。
    2>DP算法黄金准则2最优子问题—— 子问题的最优解可以推出原问题的最优解。

    我们还是来看上面的那个决策树,很明显,DP的本质就在于缓存。我们寻找DP结果的时候,往往是需要遍历这个树,从中找出最优解。但是有些情况下,我们需要寻找的不是最优解,而是可行解,这个时候往往使用DFS或者循环更为有效,后面,我们会给出例子。此时,我们仅仅需要记得,动态规划的第二个条件—— 最优子问题。

    所以算法的设计思路不在于一下子就想到了某个问题可以使用DP算法,而在于先看能不能用穷举法,如果可以用问题可以分解,分治法+穷举可以解决;如果问题包含重叠字问题,并且是求解最优解,那么此时用动态规划。

  • 相关阅读:
    【从零开始学Java】第六章 运算符
    【从零开始学Java】第五章 变量和数据类型
    【从零开始学Java】第四章 常量
    【从零开始学Java】第三章 HelloWorld入门程序
    【从零开始学Java】第二章 Java语言开发环境搭建
    【从零开始学Java】第一章 开发前言
    vim配置
    神奇的洛谷运势汇总
    达哥题表
    数论总结
  • 原文地址:https://www.cnblogs.com/forcheryl/p/3986817.html
Copyright © 2011-2022 走看看