动态规划(Dynamic Programming) 傻瓜理解

动态规划(Dynamic Programming)

一个简单题目

爬楼梯
来源:力扣
难度: 简单
AC时间:<10 min

题目:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

这个题目应该大部分人很熟悉,应该是一道小学的数学题,小学的题目给了一个固定的 n n n,比如10级台阶,当时的解法是从第三阶开始,爬楼梯的方法等于前两阶的和,实际上就是下面要讲的动态规划。

当然,我们先从一个更简单的递归思路出发,如果我们需要知道第 n n n阶台阶的爬法数目,我们又知道爬到第 n − 1 n-1 n1级的爬法数目,所有爬到第 n − 1 n-1 n1级的爬法再往上爬一级都能爬到第 n n n级;同样的,我们如果知道爬到第 n − 2 n-2 n2级的爬法数目,这些爬法再往上爬2级都能爬到第 n n n级。

同时从第 n − 1 n-1 n1级爬到第 n n n级的最后一步是爬一级台阶,从第 n − 2 n-2 n2级爬到第 n n n级的最后一步是爬两级台阶,所以这两种爬法不会重复,因而:爬第 n n n级的爬法数量 = 爬第 n − 1 n-1 n1级的爬法数量 + + + 爬第 n − 2 n-2 n2级的爬法数量。

递归表达式有了,我们只需要一个递归的终止条件,显然:爬一级台阶只有一种爬法,爬二级台阶有两种爬法,这就是递归的终止条件。

递归代码如下:

class Solution {      
  public int climbStairs(int n) {            
    if(n == 1 || n == 2 )            
      return n;        
    else            
      return climbStairs(n-1) + climbStairs(n-2);    
  }
}

不足:
如果稍微对这段代码进行分析,就会发现,代码重复计算了很多东西:

例如当 n = 20 n=20 n=20 时,第一次递归时计算了 n = 19 n=19 n=19 n = 18 n=18 n=18 的爬法数量。但是当我们计算 n = 19 n=19 n=19 的爬法数量时,又会再计算一次 n = 18 n=18 n=18 的爬法数量。这样算法会耗费很多时间来计算重复的项,显然还有优化的空间。

改进1:
一个比较直观的想法是,每次我计算出了爬到第 k k k 级台阶的爬法数量的时候,我拿个小本本记录下来,等到下次再计算到相同的台阶爬法数量的时候,我就可以从小本本里把这个结果拿出来,不用再费力地去计算了。这其实就是优化递归的一种 备忘录 方法。

改进2:
另外一种想法是,这题目好像很像斐波那契数列,那我可不可以把从第 1 1 1 级开始,把每一项都计算出来呢?显然是可以的!我们可以使用一个数组 dp,将第0项和第1项初始化,根据斐波那契数列的递推式,计算并记录每一项的值。

改进3:
我们发现改进2还是需要使用数组来储存结果,有没有办法不储存这么多结果呢?有!既然每一项只依赖与它之前的两项,那么我们每次就记录最后的两项,再用它去递推下一项,并且用递推的结果来更新我们的记录即可。

class Solution {
    public int climbStairs(int n) {
        int begin1 = 1;
        int begin2 = 2;
        int num = 2;
        while(num<n)
        {
            begin2 += begin1;
            begin1 = begin2- begin1;
            num++;

        }
        if( n == 1 )
            return 1;
        else
            return begin2;
    }
}

这改进2和改进3都可以称之为动态规划。

一个稍微复杂一丁点的题目

看了上面的解题,是不是觉得动态规划也不是很复杂,下面可以稍微尝试一下复杂一丁点的题目。

最长回文子串
来源:力扣
难度:中等
AC时间:45-60分钟

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

分析

显而易见,任何一个长度为 1 1 1的字符串都是回文串,而任何一个长度为2的字符串是回文串的条件是两个字符是相同的。

而对于任何长度超过2(即 ≥ 3 \ge3 3)的回文串,如果把其第一个字符和最后一个字符同时删除,剩下的字符串也必然是一个回文串;同样地,如果一个回文串,在它的头部和尾部同时添加一个相同的字符,也能构成一个新的回文串。

上面的结论结合起来,已经可以构成所有的回文子串了(想一想为什么?),接下来就是找一个合适的方法来表达上面的结论了。

方法

我们使用一个二维数组dp[i][j],来储存字符串中由第 i 个字符到第 j 个字符构成的子串是不是回文子串,如果不是,数组值置0,如果是,数组值置子串长度(即 j − i + 1 j-i+1 ji+1)。

显然每一个dp[i][i]的值都为1,dp[i][i+1]的值我们也可以通过遍历来进行 0 0 0 或者 2 2 2 的赋值。

接着,对于每一个dp[i][j],我们需要首先看一下dp[i+1][j-1]的值是否不为0,如果为0则将dp[i][j]也置0,如果不为0,再看一下第i个字符和第j个字符是否相同,不相同则置0,相同则置 dp[i+1][j-1]+2。

看起来很简单是不是,但是我们仔细想一想,如果要确保这个算法成功,我们需要保证在计算每一个dp[i][j]之前,dp[i+1][j-1]就已经被计算出来了。而如果用简单的两层循环遍历i和j的话,会出现dp[i+1][j-1]没有被计算出来的情况。

例如当 i = 5,j = 5时,即字符串中有5个字符时,我们有dp[0][0], dp[1][1],dp[2][2],dp[3][3],dp[4][4],dp[0][1], dp[1][2], dp[2][3], dp[3][4]。

如果用遍历i和j的两层循环时,我们首先计算dp[0][2]的时候,会用到dp[1][1],这没问题,因为dp[1][1]已经被计算出来了。我们计算dp[0][3]的时候,我们需要dp[1][2],也ok。但是下一步我们计算dp[0][4]的时候,发现dp[1][3]好像并没有被计算出来。

所以回到思路再想一想,发现我们应该根据字符串长度来进行一步一步计算。即首先初始化了长度为1和2的子串的值,再从长度 n = 3 n=3 n=3 开始,一步一步将所有长度为 n n n 的子串对于的数组中的值全部计算出来,才能计算下一长度为 n + 1 n+1 n+1 的子串对应的值。

代码

class Solution {
    public String longestPalindrome(String s) {
        int length = s.length();
        if(length == 0)
            return "";
        int[][] dp = new int[length][length];
        for(int i = 0 ; i<length ; i++){
            for(int j = 0 ; j< length ; j++){
                dp[i][j] = 0;
                }
            }
        for(int i = 0 ; i<length ; i++){
            dp[i][i] = 1;
        }
        for(int i = 0 ; i < length-1 ; i++){
            if(s.charAt(i) == s.charAt(i+1))
                dp[i][i+1] = 1;
        }
        for(int gap = 2 ; gap <length ; gap++){
            for(int i = 0 ; i < length  ; i++){
                if(i+gap<length && dp[i+1][i+gap-1] != 0 && s.charAt(i) == s.charAt(i+gap))
                    dp[i][i+gap] = 1;
            }
        }
        String answer = "";
        int len = 0;
        for(int i = 0 ; i<length ; i++){
            for(int j = 0 ; j< length ; j++){
                if(dp[i][j] != 0 && j-i+1 > len){
                    len = j-i+1;
                    answer = s.substring(i,j+1);
                }
            }
        }
        return answer;
    }
}

其实这段代码有很多冗余,可以精简精简。使得逻辑更加清晰,代码更加简洁!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值