算法思想(一)——动态规划基础

一直觉得无论是背多少遍排序,背多少leetcode的题目,都是舍本逐末的,这些是术不是道,算法思想才是根本,但是所谓的普遍规律在教材上通常都是抽象和晦涩的,算法思想给我的感觉也是如此,《算法导论》里用来描述它的语言十分地生涩,可是如果希望以后在遇到算法笔试或者是面试手撕算法题时不再无从下手,也只能硬着头皮上了。本篇讨论动态规划的概念性问题。

动态规划基础

最优子结构

1.什么时候用

算法导论种介绍,适合采用动态规划方法的最优化问题需要具备两个要素:最优子结构和重叠子问题。最优子结构表明一个问题的最优解中包含了子问题的最优解。

2.寻找最优子结构

在寻找最优子结构时,可以遵循一种共同的模式:

  • 1)问题的一个解可以是做一种选择,例如在汽车装配问题种的选择一个装配线装配站,或者矩阵分裂问题中的选择一个下标以分裂矩阵,总之这种选择可以得到一个或者多个待解决的子问题
  • 2)假设对于一个给定的问题,已知一个导致最优解的选择,而不必关心这个选择是如何确定的,尽管假定它是已知的;
  • 3)在已知这种选择后,要确定哪些子问题会随之而来,以及如何最好地描述所得到的子问题空间。
  • 4)利用一种“剪贴”技术,来证明在问题的一个最优解中,使用的子问题的解本身也是最优的。通过假设每个子问题的解都不是最优的,然后导出矛盾,即可做到这一点。特别地,通过“剪除”非最优的子问题解再“贴上”最优解,就证明了可以得到原问题的一个更好的解,因此,这与假设已经得到一个最优解相矛盾。如果有多余一个子问题的话,由于它们通常非常类似,所以只要对其中一个子问题的“剪贴”略作修改,就可以很容易地适用于其他子问题。

最优子结构再问题域中以两种方式变化:

  1. 有多少个子问题被使用在原问题的一个最优解中,以及
  2. 在决定一个最优解中使用哪些子问题时有多少个选择。

非正式地,一个动态规划算法地运行时间取决于两个因素地乘积:子问题的总个数和每个子问题中有多少个选择
动态规划以自底向上的方式来利用最优子结构。也就是说,首先找到子问题的最优解,解决子问题,然后找到问题的一个最优解。寻找问题的一个最优解需要在子问题中做出选择,即选择哪一个来求解问题,问题的代价通常是子问题的代价加上选择本身带来的开销。
值得注意的是,“贪心算法”适用的问题同样具有最优子结构,但它与动态规划最大的区别在于以自顶向下的方式使用最优子结构,贪心算法会先做选择,选择当时看起来最优的,然后再求解一个结果子问题,而不是先寻找子问题的最优解,再做选择。

Attention Here:我们需要注意一些不能应用最优子结构的地方,绝不能假设它能够使用。已知一个有向图G=(V,E)和结点u,v∈V,考虑以下两个问题。
无权最短路径:找出一条从u到v的包含最少边数的路径。这样的一条路径必须是简单路径,因为从路径中去掉一个回路后,会产生边数更少的路径。
无权最长简单路径:找出一条从u到v的包含最多边数的简单路径。我们需要加入简单性的要求,否则的话,就可以随意遍历一个回路无数次,得到有任意边数的路径。

3.重叠子问题

适用于动态规划求解的最优化问题必须具有的第二个要素是子问题的空间要”很小“,也就是说,用来求解原问题的递归算法可反复地解**同样的子问题,而不是总在产生新的子问题。对于产生不同的子问题的情况,它的子问题数就是输入规模的一个多项式。
当一个递归算法不断地调用同一问题时,我们说该最优化问题包含重叠子问题,而相反地,适合用分治法解决地问题往往在递归的每一步都会产生一个全新的子问题
动态规划算法总是充分地利用重叠子问题,每个子问题只解一次并将子问题的解存储在一个辅助表中,每次查表地时间为常数。

4.重新构造一个最优解

每个子问题只解一次并将子问题的解存储在一个辅助表中,这样在需要时,我们就不必根据已经存储下来的代价信息来重构这方面的信息了。

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

我们通过一个最长公共子序列的问题来演示动态规划算法的应用。在生物信息学中,人们常常需要比较不同生物大分子序列(例如DNA和蛋白质序列,根据基础的构造单元脱氧核糖核苷酸/氨基酸被它们被表示为字符串),一种合适的衡量标准就是比较它们之间相同的公共部分——它们的构造单元以相同顺序出现但不必连续,也称子串,子串越长则相似度越高。
这类问题统称为最长公共子序列问题。对于一个给定序列,它的子序列就是该序列去掉一个或多个元素。严谨地公式化地表述就是,给定一个序列X={x1,x2,…,xm},另一个序列Z={z1,z2,…,zk}时X的一个子序列,如果存在X的一个严格递增下标序列{i1,i2,…,ik},使得所有的j=1,2,…,k,都有xij=zj。
最长公共子序列问题的解决分为以下步骤。

1.描述一个最长公共子序列

定理 :LCS最优子结构

设X={x1,x2,…,xm}和Y={y1,y2,…,yn}为两个序列,Z={z1,z2,…,zk}为X和Y的任意一个LCS。

  • 1)如果xm=yn,那么zk=xm=yn而且Zk-1是Xm-1和Yn-1的一个LCS。
  • 2)如果xm!=yn,那么zk!=xm蕴含Z是Zk是Xm-1和Y的一个LCS。
  • 3)如果xm!=yn,那么zk!=yn蕴含Z是Zk是Xn-1和Y的一个LCS。

证明

  • 1)如果zk!=xm,那么可以添加一项xm=yn到Z中,就得到了一个长度为k+1的比Z长的LCS,这与Z是X和Y的LCS是矛盾的。
  • 2)假设Xm-1和Y有一个长度大于k的公共子序列W,那么W也应该是X和Y的一个公共子序列,这与Z是X和Y的LCS是矛盾的。
  • 3)和2)情况对称。

2.一个递归解

由上述定理可知,在寻找X和Y的LCS时,可能要检查一个或两个子问题:

  • 如果xm=yn,必须找出Xm-1和Yn-1的一个LCS,将xm=yn添加到这个LCS上,即可得到X和Y的LCS;
  • 而如果xm!=yn,就必须解决两个子问题:找出X和Yn-1的一个LCS,以及Xm-1和Y的一个LCS。这两个LCS中,较长的就是X和Y的LCS。

定义c[i,j]为Xi和Yj的一个LCS的长度,则可得关于求LCS长度的递归式:

c [ i , j ] = { 0 i = 0 o r j = 0 c [ i − 1 , j − 1 ] + 1 i , j > 0 a n d x i = y j m a x ( c [ i , j − 1 ] , c [ i − 1 , j ] ) i , j > 0 a n d x i ≠ y j c[i,j]=\begin{cases} 0 & i=0 &or&j=0\\ c[i-1,j-1]+1 &i,j>0&and&x_i=y_j\\ max(c[i,j-1],c[i-1,j])&i,j>0&and&x_i≠y_j\\ \end{cases} c[i,j]=0c[i1,j1]+1max(c[i,j1],c[i1,j])i=0i,j>0i,j>0orandandj=0xi=yjxi̸=yj

3.计算LCS的长度

根据上一步中的递归公式,我们很容易就能写出一个递归算法来计算LCS的长度,因为只有 Θ ( m n ) \Theta(mn) Θ(mn)个子问题,所以可以采用动态规划自底向上计算。

public int[][] lcsLength(String x, String y) {
        char[] xArr = x.toCharArray();
        char[] yArr = y.toCharArray();
        int m = xArr.length;
        int n = yArr.length;
        int[][] c=new int [m][n];
        lcsLength(xArr,yArr,m,n,c);
        return c;
    }

    private int lcsLength(char[] xArr, char[] yArr, int m, int n,int[][] c) {
        if (m == 0 || n == 0)
            c[m][n]= 0;
        if (xArr[m] == yArr[n]) {
            c[m][n]= lcsLength(xArr, yArr, m - 1, n - 1,c) ;
            return c[m][n];
        } else{
            c[m][n]= Math.max(lcsLength(xArr, yArr, m-1, n,c),lcsLength(xArr, yArr, m, n-1,c));
            return c[m][n];
        }
    }

4.构造一个LCS

//todo
3、4两步应该时写代码来实现了。有空来写一遍贴上来。先放着

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值