zoukankan      html  css  js  c++  java
  • 算法整理之动态规划

    我现在介绍的这个版本,是从算法爱好者中看到的一个别人的漫画版本。
    题目:
    有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种
    走法。
    比如,每次走1级台阶,一共走10步,这是其中一种走法。我们可以简写成 1,1,1,1,1,1,1,1,1,1。
    再比如,每次走2级台阶,一共走5步,这是另一种走法。我们可以简写成 2,2,2,2,2。

    分析:
    这种问题是典型的使用动态规划解决的问题,在使用动态规划的方法之前,能想到的方法可能就是使用排列组合,
    这是一个非常复杂的嵌套循环,俗称暴力枚举,不提倡使用。
    [动态规划]dynamic programming是一种分阶段求解决策问题的数学思想。它不知用于变成领域,也应用在管理学
    经济学和生物学中。简言之,“大事化小,小事化了”。刚才的题目简单分析如下:
    1)当还差1步走完10级台阶的时候,这时有两种情况i)剩下1级台阶:9->10,ii)剩下两级台阶:8->10.暂且不说,
    如果从1->8,1->9分别有X,Y中方法,那么原问题(一共有多少种走法)结果就是X+Y
    2)把上面的分析形式化一下就是,我们要求的,F(9)=Y,F(8)=X,F(10)=F(9)+F(8),分析求解F(9),F(8)的过程又和
    上面是分析求f(10)的过程是相似的,F(9)=f(8)+F(7)...上面的过程就是在把一个复杂的问题分蘖阶段进行简化,
    逐步简化成简单的问题,这就是动态规划的思想:大事化小,化繁为简.
    3)进一步往前推,显然
    F(1)=1
    F(2)=2
    F(n)=F(n-1)+F(n-2) (n>=3)

    动态规划中有三个非常中澳的概念:【最优子结构】、【边界】、【状态转移公式】
    在F(10)=F(9)+F(8)中,F(9)和F(8)就是F(10)的【最优子结构】
    当只有1级台阶或者级台阶时我们可以直接得出结果,无需继续花间,我们成F(1)F(2)是问题的【边界】
    F(n)=F(n-1)+F(n-2) (n>=3)是阶段与阶段之间的【状态转移方程】这是动态规划的核心

    解法分析与升级:
    解法一:朴素的递归解法
    getClimbingWyas1 是一个基础的递归解法,这个方法就是一个很朴素的递归求解,复杂度较高,这里顺带介绍
    了递归复杂度的粗略估计方法:做出递归求解的递归树,发现就是一个二叉树,其中每一个节点就是程序需要计算
    的,那么大致复杂度就是 O(2^n)

    解法二:升级版的递归解法,降低复杂度-----备忘录递归
    之所以会有那么糟糕的复杂度,就是因为认真观察上面列出的递归树,会发现,有很多节点是被重复计算了,为了
    避免这种重复计算,通常使用的做法是使用一个哈希缓存:先创建一个缓存,每次吧不同参数的计算结果存入哈希表
    当遇到相同的参数是,再从哈希表中取出,节省了计算时间。这种方法有个专业名字【备忘录算法】,改进后的算法
    实现为getClimbingWays2(int).
    使用哈细胞改造后的,复杂度分析:从F(1)到F(N)一共是N个不同的输入,在哈希表里面存储了N-2个结果,所以时间
    复杂度和空间复杂度都是好O(N)

    貌似【备忘录】结合不同的题目会有不同的,形式,我记得当年看到的一些题目里面用的好像是二维数组,当年傻傻
    的不明白。

    解法三:【正真的动态规划求解】
    对上面的递归球阀进一步的,优化的结果
    前面的递归方法是一种”自定向下“的解法,逆向思维,使用自底向上的思想来优化:
    所谓的自底向上的思路:当 n>=3时,F(N)之和 前面的F(n-1)和F(n-2)有关,所以,只需要用前面两个状态迭代出
    后面的第三个状态就行了。于是以前的递归就变成了一个为递归方法getclimbingWays3(int n)
    瞬间时间复杂度变成了o(N) 空间复杂度变成了O(1)

     1 package dynamicprogram;
     2 
     3 import java.util.HashMap;
     4 import java.util.Map;
     5 
     6 /*
     7  * 这里主要是用来讲解动态规划算法的10级台阶问题的编码,与求解的优化
     8  */
     9 public class TenStairs {
    10 
    11     /*
    12      * 使用递归求解的基础版
    13      */
    14     public static int getClimbingWays1(int n)
    15     {
    16         
    17         if ( n<0 )
    18             return 0;
    19         else if (n ==1 )
    20             return 1;
    21         else if ( n == 2)
    22             return 2;
    23         else
    24             return getClimbingWays1(n-1)+getClimbingWays1(n-2);
    25     }
    26     
    27     
    28     /*
    29      * 使用 备忘录算法给今后的 递归方法,
    30      * 这里递归程序的编写有一个小技巧,为了不定义一个static的map,把map作为递归
    31      * 函数的一个参数,一层层的向下传递
    32      */
    33     public static int getClimbingWays2( int n, Map<Integer, Integer> map)
    34     {
    35         if ( n<0 ) return 0;
    36         if ( n == 1 ) return 1;
    37         if ( n == 2 ) return 2;
    38         
    39         if ( map.containsKey(n))
    40             return map.get(n);
    41         else{
    42             int value = getClimbingWays2(n-1, map) + getClimbingWays2(n-2, map);
    43             map.put(n, value);
    44             return value;
    45         }
    46     }
    47     
    48     /*
    49      * 使用自底向上的迭代方法
    50      */
    51     public static int getClimbingWays3(int n)
    52     {
    53         if ( n<1 ) return 0;
    54         if ( n == 1) return 1;
    55         if ( n == 2) return 2;
    56         
    57         int a = 1;
    58         int b = 2;
    59         int tmp = 0;
    60         
    61         for ( int i=3; i <= n; i ++){
    62             tmp = a + b;
    63             a = b; 
    64             b = tmp;
    65         }
    66         return tmp;
    67     }
    68     
    69     public static void main(String[] args) {
    70         int ways = 0;
    71         Map<Integer, Integer> map = new HashMap<Integer, Integer>();
    72         
    73         long start = System.currentTimeMillis();
    74         ways = getClimbingWays3(10);
    75         
    76         long end = System.currentTimeMillis();
    77         
    78         System.out.println(ways);
    79         System.out.println((end-start)/1000);
    80         
    81     }
    82 }
    View Code

    上面的爬楼梯只是动态规划中比较简单的一个例子,规划的维度只有一个,下面给出一个较为复杂的一个

    题目二:过往和金矿的问题( get most gold)
    有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是
    10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。
    各个金矿的投入和产出: 400金/5人, 500金/5人, 200金/3人, 300金/4人, 350金/3人
    要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?
    分析:
    拿到一个动态规划的问题,都要考虑其核心的三要素:【最优子结构】、【边界】、【状态转移方程】
    寻找最优子结构,我们往往倒着来。
    为了便于描述,这里设金矿数量为n,工人数为w,出尽量为g[],每一座金矿的用工数为p[].
    那么5座金矿和4座金矿之间的最优解存在这样的关系,F(5, 10) = MAX( F(4,10), F(4,10-p[4])+g[4] )
    边界问题,就是考虑只有一座金矿的时候,又要考虑下面两种情况呢:
    n=1, w>=p[0], f(n,w)=g[0]
    n=1, w<p[0], f(n,w)=0
    于是整理之后就可以得到状态方程如下:
    F(n,w) = 0 (n<=1, w<p[0]);
    F(n,w) = g[0] (n==1, w>=p[0]);
    F(n,w) = F(n-1,w) (n>1, w<p[n-1])
    F(n,w) = max(F(n-1,w), F(n-1,w-p[n-1])+g[n-1]) (n>1, w>=p[n-1])
    其实有了具体的状态转移方程,就能够很快的写出朴素的递归解法。几种常见的解法见下:

    解法一:排列组合,暴力枚举
    每一座金矿都有挖与不挖两种选择,如果有N座金矿,很明显就有2^N中选择,对所有可能的情况做边淋,排除那些
    用人总数超出10的选择,在剩下的选择里寻找获取金币最多的。这种方法没有太多好说的。
    方法的时间复杂度,是O(2^n)

    解法二:使用朴素的递归方法。
    很简单,就是把状态转移方程翻译成代码即可,时间复杂度就是递归树的节点个数数量级O(2^n)

    解法三:使用备忘录算法:
    这种方法在简单的递归基础上创建一个hashmap备忘录,hashmao的key是一个包含了金矿数n和工人数w的对象,value
    是最优选择获得的黄金数。

    解法四:真正的动态规划方法的实现。
    在上一个例子是一维的,转化为自底向上的迭代的时候就是简单的,在一维显性平面上的迭代。
    当前的这个问题因为有两个量在变,这时候,寻找迭代关系就是在一个二维表中,其实这都很固定的分析问题的方法
    类似的问题都可以如此解决。
    使用转台庄毅方程,填写下面的状态转移表,其实就是上面map总的内容
    1工人 2工人 3工人 4工人 5工人 6工人 7工人 8工人 9工人 10工人
    1金矿 0 0 0 0 400 400 400 400 400 400
    2金矿 0 0 0 0 500 500 500 500 500 900
    3金矿 0 0 200 200 500 500 500 700 700 900
    4金矿 0 0 200 300 500 500 500 700 800 900
    5金矿 0 0 350 350 500 550 650 850 850 900
    观察上面的状态转移表,可以发现,除了第1行以为,每个格子都是前一行的一个或者两个格子推到出来的,所以程序
    实现时,只需要存储一行结果,就可以推导出新的一行。
    这种方法的复杂度是O(n*w)当n,w较大时效率不及朴素的递归运算。

      1 package dynamicprogram;
      2 
      3 import java.sql.Array;
      4 import java.util.Arrays;
      5 import java.util.HashMap;
      6 import java.util.Map;
      7 
      8 /*
      9  * 题目二:掘金问题
     10  */
     11 
     12 public class MostGold {
     13 
     14     /*
     15      * 使用暴力枚举的方法
     16      */
     17     public static int getMostGold1(int n, int w, int p[], int g[])
     18     {
     19         int most = 0;
     20         String str = "00000";
     21         int N = (int) Math.pow(2, n)-1;    // 一共这么多种情况
     22         for (int i=0; i<=N; i++)
     23         {
     24             int result = 0;
     25             String r1 = Integer.toBinaryString((i&N));
     26             r1 = new StringBuffer(r1).reverse().toString();
     27             String strTmp = str.substring(r1.length(), str.length());
     28             r1 = r1+strTmp;
     29             int pc = 0;
     30             for (int index=0; index < r1.length(); index ++)
     31             {
     32                 int flg = r1.charAt(index)-'0';
     33                 pc = pc + p[index]*flg;
     34                 result = result + g[index]*flg;
     35             }
     36             
     37             if ( pc==10 )
     38                 System.out.println("i = " + i + ", pc = " + pc + ", reslut = "+result);
     39         }
     40         return most;
     41     }
     42     
     43     /*
     44      * 使用 朴素的递归方法
     45      */
     46     public static int getMostGold2(int n, int w, int p[], int g[])
     47     {
     48         if ( n<=1 && w<p[0] ) return 0;
     49         if ( n==1 && w>=p[0] ) return g[0];
     50         if ( n>1 && w<p[n-1] ) return getMostGold2(n-1, w, p, g);
     51         else
     52             return Math.max(getMostGold2(n-1, w, p, g), getMostGold2(n-1, w-p[n-1], p, g)+g[n-1]);
     53     }
     54     
     55     /*
     56      * 使用 带有hash备忘录的 改进递归方法
     57      * 为了 实现使用hash方法这里,把包含的金矿个数n和工人数w构造成一个对象,为此写一个内部类,在这个方法的下面
     58      */
     59     public static int getMostGold3(int n, int w, int p[], int g[], Map< Key, Integer> map)
     60     {
     61         if ( n<=1 && w<p[0] ) return 0;
     62         if ( n==1 && w>=p[0] ) return g[0];
     63         
     64         Key key = new Key(n-1, w);
     65         int value;
     66         if ( n>1 && w<p[n-1] )
     67         {
     68             if (map.containsKey(key))
     69                 value = map.get(key);
     70             else{
     71                 value = getMostGold2(n-1, w, p, g);
     72                 map.put(key, value);
     73             }
     74         }
     75         else
     76         {
     77             if (map.containsKey(key))
     78                 value = map.get(key);
     79             else
     80             {
     81                 value = Math.max(getMostGold2(n-1, w, p, g), getMostGold2(n-1, w-p[n-1], p, g)+g[n-1]);
     82                 map.put(key, value);
     83             }
     84             
     85         }
     86         return value;
     87     }
     88     
     89     /**
     90      * 真正的 动态规划的实现 (就是迭代版的解法)
     91      * 如果不是自己亲自写一下,你不知道,这么短小的一段程序会花费你自己多少时间,
     92      * 伪代码描述的知识思路,重要的还是自己去写,去实现
     93      * @param args
     94      */
     95     public static int getMostGold3(int n, int w, int p[], int g[])
     96     {
     97         int[] preResults = new int[w+1];    //为了处理的方便,加了0行0列
     98         
     99         // 填充边界情况,也就是只有1个金矿的情况
    100         for ( int j=1; j <= w; j++){
    101             if ( j < p[0] )
    102             {
    103                 preResults[j] = 0;
    104             }else{
    105                 preResults[j] = g[0];
    106             }
    107         }
    108         System.out.println(Arrays.toString(preResults));
    109         
    110         // 填充 其余格子, n>1
    111         for ( int i = 2; i <= n; i ++)    // 从第二行开始
    112         {
    113             int[] results = new int[w+1];    // 计算下一行
    114             for (int j = 1; j <= w; j ++){
    115                 if ( j < p[i-1] )
    116                     results[j] = preResults[j];
    117                 else
    118                     results[j] = Math.max(preResults[j], preResults[j-p[i-1]]+g[i-1]);
    119             }
    120             preResults = results;
    121             System.out.println(Arrays.toString(preResults));
    122         }
    123         return preResults[w];
    124     }
    125     
    126     public static void main(String[] args) {
    127         int g[] = {400, 500, 200, 300, 350};    // 每矿出金量
    128         int p[] = {5, 5, 3, 4, 3};                //每矿用工数
    129         Map< Key, Integer> map = new HashMap<Key, Integer>();
    130         
    131         System.out.println(getMostGold3(5, 10, p, g));
    132     }
    133 }
    134 class Key{
    135     int n;
    136     int w;
    137     
    138     public Key(int n, int w)
    139     {
    140         this.n = n;
    141         this.w = w;
    142     }
    143     
    144     @Override
    145     public int hashCode() {
    146         // TODO Auto-generated method stub
    147         return n*100;
    148     }
    149     @Override
    150     public boolean equals(Object obj) {
    151         Key key = (Key) obj;
    152         if ( this.n == key.n && this.w == key.w) return true;
    153         else return false;
    154     }
    155 }
    View Code
  • 相关阅读:
    iOS 开发网络篇—监测网络状态
    再杀掉app之后 删除NSUserDefault存在本地的数据
    iOS开发之duplicate symbols for architecture x86_64错误
    iOS中UITextField输入判断小数点后两位
    ios 适配iOS11&iPhoneX的一些坑
    iOS UITextView 设置 NSLinkAttributeName 属性,点击链接跳转
    iOS- UITextView与键盘回收与键盘遮挡输入框
    iOS 实现单个页面支持横竖屏,其他页面只能竖屏
    iOS最新Mac OS X 10.11之后 安装cocoapods及使用详解
    一种简单的登录加密方案
  • 原文地址:https://www.cnblogs.com/OliverZhang/p/7185385.html
Copyright © 2011-2022 走看看