五大经典算法-动态规划 及其算法应用

Posted 踩踩踩从踩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了五大经典算法-动态规划 及其算法应用相关的知识,希望对你有一定的参考价值。

前言

整篇文章分析整个动态规划算法,什么是动态规划,及动态规划算法在字符串匹配中使用、分治法的差别点、动态规划优点;

概念

什么叫做动态规划(dynamic programming),它是运筹学的一个分支,是求解决策过程最优化的数学方法。20世纪50年代数学家 R.E.Bellman等人 在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理,把多阶段过程转换为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法-动态规划;应用在计算最短路径、库存管理、资源分配、设备更新、排序、装载等问题

特点

分析是由大到小,写代码是从小到大,计算过程中会把结果进行记录,最终结果在记录中找到

怎么去理解这个,从下面的基本思想中理解,我觉得大家就能理解了

基本思想 

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

基本代码的实现

分治法一个简单的实现,之前用栈去解决斐波拉契数列这些问题,现在我们用动态规划去解决问题,用空间复杂度去换时间复杂度

原有栈的方式

    public static double f(int n){
        if(n==1 || n==2){
            return 1;
        }else{
            return f(n-1)+f(n-2);
        }
    }

这种方式代码简单,但性能是非常不好的,一旦数据量大了之后这种方式时间消耗相当大,尤其是某些业务场景下,要求效率非常高;因此引入下面的代码动态规划,时间效率提高

public static double f1(int n){

double[] array=new double[n];
double[0]=1;
double[1]=1;
for(int i=2;i<array.length;i++){
   array[i]=array[i-1]+array[i-2];
}
return array[n-1];
}

这就是通过动态规划去求最优化的解,这段代码的实现,小问题的结果相关联。分析过程是由大到小,而算法是由小到大的,通过存储空间记录,我们就能得到答案

总结

动态规划是求解具有某种最优性质的问题(子问题不是相互独立),并且和分治法一样都是将问题进行拆分,并且将拆分问题答案进行记录,从计算出最优的结果。明显可以看出动态规划是会多占用内存的,以空间复杂度来换取问题答案。

算法应用

最长公共子序列(Longest Common Subsequence,lcs)

这个应用在生活中应用的非常多,例如当前这个应用,就是对比数据的相似,截取公共部分,百度知道,相似度的比较:计算生物学DNA对比,图形相似处理,媒体流的相似比较,WEB页面中找非法言论等等

定义

一个序列A删除若干个字符得到新的序列B,而B就是A的子序列

 这样一删除字符过后就能获得到子序列

两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列

  例如X={A,B,C,B,D,A,B},Y={B,D,C,A,B,A}则它们的lcs是{B,C,B,A}和{B,D,A,B}。求出一个即可。

这和顺序有关,相同的值的最大长度,并不管位置。怎么用动态规划去求出最长公共子序列,这就是我们动态规划去研究的,除了动态规划,我们还有什么方法,那当然是穷举法,这个最恶心,在于拿着一个字符一个字符,一位一位的去比对,就会找到,但性能糟糕,没法用的。

算法分析

我们用下面例子进行分析

X={B,D,C,A,B,A},Y={A,B,C,B,D,A,B}则它们的lcs是{B,C,B,A}和{B,D,A,B}。求出一个即可。

我们用上面 的来表示 ,比如Y4=B X4=A  通过这样来分析lcs求解 

  •  字符串X,长度m,从1开始计数
  • 字符串Y,长度n,从1开始计数
  • Xi=<x1,x2,...,xi>表示X序列的前i个字符(1<=i<=m)
  • Yj=<y1,y2,...,yj>表示Y序列的前j个字符(1<=j<=n)
  • LCS(X,Y)为字符串X和Y的最长公共子序列,其中的一个解为Z=<z1,z2,....zk>
  •   注意:LCS(X,Y)(最长公共子序列)其实为一个集合,Z为一个解

就像我在刚才分析那样,最长的公共子序列有多个解,所以是一个集合来存储,而Z就是其中的一个解

思路解析

首先取两个字符串的末尾进行取一个字符对比

 如果两个字符串不相等则有两种情况下面

  • 第一种X去除末尾字符进行对比

  •  Y去掉末尾字符进行对比

 如果最后一位是相同的,那就是下面种情况

 就这样重复往前步骤,就是这样找到了最长子公共序列

 最终得到的结论就是

 这个结论

  • 如果 最后一位相等,上面个公式,最长子公共序列加上相同的xm 或者 ym
  • 如果最后一位不相等,取xm-1和yn的公共子序列和 xm和yn-1的公共子序列的最大的公共子序列,这就能找到最长公共子序列

代码分析与编码

我们需要建立一个二维数组,相同的取左上加1,不同取上和左的最大值 ,这也是从上面

 首位都填空字符串,都不填位置,

 

 按照规则不断填,就能填出上面的值,然后就能找到公共子序列的长度。这就是动态规划的算法,从前往后推,记录好之前的数据,不断往前推,而在去取长度。有规律,只要数据加长一位时,则表明相同的字符。

我们继续寻找最长的公共子序列,从后往前推,这样就能找到一个公共子序列,ABCB ,在反过来就是了 和BADB 

 先从建立一个小方法开始

  • 写一个字符长度不同的时求最大值
    public static int max(int a,int b){
        return (a>b)?a:b;
    }
  •  建立核心方法getLCS 并创建一个二维数组 传入 x 和y字符串,用于后面的操作
 char[] s1=x.toCharArray();
        char[] s2=y.toCharArray();
        int[][] array=new int[x.length()+1][y.length()+1];
  •  然后就按照我之前构造的数组,做个数据填充,当然也可以不填充
 for (int i = 0; i < array[0].length; i++) {
            array[0][i]=0;
        }
        for (int i = 0; i < array.length; i++) {
            array[i][0]=0;
        }
  • 使用动态规划的方式填入数据,一格一格判断字符串 这里需要减一次比较 ,s1和s2是原字符,相等因为在数组上数据是从1开始的,所以不能按照数组来。就已经能得到刚才的矩阵了
 for (int i = 1; i < array.length; i++) {
            for (int j = 1; j < array[i].length; j++) {
                if(s1[i-1]==s2[j-1]){//如果相等,左上角加1填入
                    array[i][j]=array[i-1][j-1]+1;
                }else{
                    array[i][j]=max(array[i-1][j],array[i][j-1]);
                }
            }
        }
  • 然后继续往前找结果,去找到数据,先搞一个栈进行存储,因为是反过来
Stack result=new Stack();
  • 往前找数据
 int i=x.length()-1;
        int j=y.length()-1;
        while((i>=0) && (j>=0)){
            if(s1[i]==s2[j]){
                result.push(s1[i]);
                i--;
                j--;
            }else{//注意数组和String中的位置有一位差
                if(array[i+1][j+1-1]>array[i+1-1][j+1]){
                    j--;
                }else{
                    i--;
                }
            }
        }

这样求最长公共字串我们就能找到了,动态规划就能实现了

KMP算法(改进的字符串匹配算法)

kmp算法和lcs算法不同点在于撒,也就是lcs算法,在匹配两个字符串的相似度,而kmp算法也就是我们在java种字符串进行匹配,只有字符串在目标字符串中能找到时,才算是ok的。

是一种改进的字符串匹配算法,由由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特-莫里斯-普拉特操作(简称kmp算法)

他的时间复杂度能达到o(m+n) 是相当快的, KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

采用穷举法则是下面的情况

 这是大家都能想通的方式,但可以想象,我们jdk肯定不能这么简单的,那效率速度有多慢。

还有一种

  

将目标串先进行循环 对比的方式,最后两位和前面两位相同,然后下次倒退,倒退到前ab然后继续对比,这样也很消耗 性能,效率也很慢的。

接下来介绍kmp算法如何做的

既然利用动态规划的特性,需要用一个数组来帮我们存储帮助我们找到回退的次数的 假设为倒退数组next

规则

第一步 使得next[i]=j

第二步 i++

第三步判断i和j的字符是否相等

i和j的字符相等 j++; 

i和j的字符不同则 j=next[j-1];

下面继续分析字符串

  •  首先进行复制一个相同模式串,第一步先在next数组中填0,i从1开始,j从0开始,遇到不同,直接j++

  • 第二次遇到数据相同了,先赋值next,在j++

 

 

这样以此类推下去 

 

 j是不断在前三个做循环,然后 这样记录下了循环的动作,而i不断增加;

最后这个数组得到的值的特点,进行对比

 这种情况可以看到几个值相同的情况,这种,我就能发现在对比字符串时,如果对比不成功,就对比到退到哪一个节点,这就能很好的节约时间

 这个算法就是这样不断对比往里面推,一旦遇到不同 j=next[j-1]  这是j的值如果为3就跳到第三位字符上,截取过后与目标字符串进行对比,以达到成功的情况

代码实现

首先匹配串找到next数组建立kmpnext算法

   public static int[] kmpNext(String dest){
        int[] next=new int[dest.length()];
        next[0]=0;
        //开始推出next
        for(int i=1,j=0;i<dest.length();i++){
            //3
            while(j>0 && dest.charAt(j) != dest.charAt(i)){
                j=next[j-1];
            }
            //1
            if(dest.charAt(i)==dest.charAt(j)){
                j++;
            }
            //2
            next[i]=j;
        }
        return next;
    }
  • 这里做的是,先创建next数组
 int[] next=new int[dest.length()];
  • 重复下面的规则

第二步 i++

第三步判断i和j的字符是否相等

i和j的字符相等 j++; 

i和j的字符不同则 j=next[j-1];

第一步 使得next[i]=j

            //判断字符是否相同的或者不同
            while(j>0 && dest.charAt(j) != dest.charAt(i)){
                j=next[j-1];
            }
         
            if(dest.charAt(i)==dest.charAt(j)){
                j++;
            }
            // 
            next[i]=j;

然后实现kmp算法 

 for(int i=0,j=0;i<str.length();i++){
            while(j>0 && str.charAt(i) != dest.charAt(j)){
                j=next[j-1];
            }
            if(str.charAt(i)==dest.charAt(j)){
                j++;
            }
            if(j==dest.length()){//结束
                return i-j+1;
            }
        }

结束条件,最后通过i-j+1就能找到需要的位置,这个思路是相当精妙的

总结

动态规划算法应用非常广泛的,学习它的思路,在开发过程中,大家都有一个好的解决方法的思路,不要只是就用什么穷举法呀,大家都能想到,但是效率非常低,在项目中是不能使用的,小数据量还行,一旦数据量增大,时间非常长,希望这篇动态规划,通过原理和应用上讲解,能有成长。

以上是关于五大经典算法-动态规划 及其算法应用的主要内容,如果未能解决你的问题,请参考以下文章

学小实,学大事 | 动态规划算法

五大经典算法之回溯法及其应用

五大常用算法之一:动态规划算法

五大常用算法:分治动态规划贪心回溯和分支界定

五大算法之动态规划

五大常用算法 动态规划算法