动态规划详解和实战

1.动态规划

1.什么是动态规划

所谓动态规划,即通过**拆分问题,**定义问题状态和状态之间的关系,将求解目标的过程通过动态的、递归求解子问题的方式去实现。

在使用递推的过程中,往往涉及到很多重叠的子问题,其时间复杂度一般都是指数级别,所以在实际的应用中,一般都会通过记忆数组的方式来避免重复计算(一般是用一维数组或者二维数组来保存)。

2.动态规划问题如何解决

动态规划问题一般是通过逆向发现递推关系,正向计算的方式进行求解,其步骤大致如下:

  • 寻找递推关系
  • 定义记忆数组
  • 初始化记忆数组
  • 应用递推关系求解

3.实战

例一:剑指 Offer 10- II. 青蛙跳台阶问题(简单)

问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
/**
  *分析:
  *设跳上 n 级台阶有 f(n)种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1 级或 2 级台阶。
  *所以可以知道f(n)=f(n-1)+f(n-2),在不用记忆数组的情况下,可以直接用递归求解。
	*/
class Solution {
    public int numWays(int n) {
        if(n==0){
            return 1;
        }
        if(n<=2){
            return n;
        }
        return (numWays(n-1)+numWays(n-2))%1000000007;
    }
}

/**
	*时间复杂度:f(n)需要拆分成f(n-1)和f(n-2),递归深度为n-2,所以时间复杂度为O(2^n),
	*空间复杂度:O(1)
	*/
/**
	*递归逆向求解过程中进行了大量的重复计算,通过引入记忆数组,能够很大程度降低时间复杂度。
	*定义长度为n的数组,array[n-1]表示f(n),可以避免f(n)的重复计算。
*/

//1.发现递推公式 f(n)=f(n-1)+f(n-2)
class Solution {
    public int numWays(int n) {
        if(n==0){
            return 1;
        }
      	if(n<=2){
            return n;
        }
      //2.定义记忆数组
        int[] array = new int[n];
      //3.初始化记忆数组
        array[0]=1;
        array[1]=2;
      //4.通过递归公式求解
        for(int i=2;i<n;i++)
        {
            array[i]=(array[i-1]+array[i-2])%1000000007;
        }
        return array[n-1];
    }
}
//时间复杂度:O(n)
//空间复杂度:O(n)
/**
*进一步发现问题,递推公式f(n)=f(n-1)+f(n-2),求解f(n)只需要记住f(n-1)+f(n-2)即可
*/
class Solution {
    public int numWays(int n) {
        if(n==0)
            return 1;
        if(n<=2)
            return  n;
        int f1=1;
        int f2=2;
        for(int i=3;i<=n;i++)
        {
            int sum=(f1+f2)%1000000007;
            f1=f2;
            f2=sum;
        }
        return f2;
    }
}
//时间复杂度:O(n)
//空间复杂度:O(1)

例二:5. 最长回文子串(中等)

给你一个字符串 `s`,找到 `s` 中最长的回文子串。
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

方法一:

  1. 分析:如果一个子串1是回文字符串,并且这个字符串相邻的两个字符相同,那么字符串1和其相邻的字符构成的字符串2也是回文字符串
  2. 定义记忆二维数组,如果dp[i+1] [j-1]&char[i]==char[j],则dp[i] [j]也是回文字符串
  3. 初始化数组,如果字符串长度为1,那么它们肯定是回文字符串,即dp[i] [i] = true,如果字符串的长度为2,那么只需要满足char[i]==char[j],则dp[i] [j]为回文字符串
  4. 应用递推公式求解
public class Solution {
    //最长回文子串动态规划
    public String longestPalindrome(String s) {
        int length = s.length();
        if (length < 2)
            return s;
        char[] chars = s.toCharArray();
        int begin = 0;      //记录最长串的起始位置
        int maxLength = 1;  //记录最长串长度
        boolean[][] dp = new boolean[length][length];   //定义记忆数组

        //初始化记忆数组
        for (int i = 0; i < length; i++)
            dp[i][i] = true;

        //通过递推关系进行求解(注意求解顺序)
        for (int j = 1; j < length; j++)
            for (int i = 0; i < j; i++) {
                if (chars[i] == chars[j]) {
                    //字符串长度为2,进行特殊处理
                    if (j - i < 2) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                } else {
                    dp[i][j] = false;
                }

                //对当前最大子串进行记录
                if (j - i + 1 > maxLength && dp[i][j]) {
                    begin = i;
                    maxLength = j - i + 1;
                }
            }
        return s.substring(begin, begin + maxLength);
    }
}

时间复杂度:O(n^2)
空间复杂度:O(n^2)

递归顺序需要特别注意:

i<=j,所以实际需要求的数据如下图箭头所示,同时,需要求dp[i] [j],首先要知道dp[i+1]和dp[j-1]的值,通过画图分析发现,需要纠结第j列的值,必须依赖于第j-1列的相关值,所以循环是先j后i。

在这里插入图片描述

方法二:

中心拓展法:

遍历字符串的每个位置,以这些位置为中心进行拓展。规则如下:

dp[i] [i]=true;

dp[i] [i+1]= (S[i]==S[i+1])

dp[i] [j] = (dp[i+1] [j-1])&(S[i]==S[j])

class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1) {
            return "";
        }
        int start = 0, end = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expand(s, i, i);//奇数中心拓展
            int len2 = expand(s, i, i + 1);//偶数中心拓展
            int len = Math.max(len1, len2);
            if (len > end - start) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end + 1);
    }

  //计算以left和right为中心的回文字符串的最长长度
    public int expand(String s, int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            --left;
            ++right;
        }
        return right - left - 1;
    }
}

例三:62. 不同路径

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yn97Yub7-1637229243486)(leetcode.assets/image-20211118170648565.png)]

二维记忆数组实现方式:

dp[i] [j]=dp[i-1] [j]+dp[i] [j-1];

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < n; i++) dp[0][i] = 1;   //初始化记忆数组
        for (int i = 0; i < m; i++) dp[i][0] = 1;   //初始化记忆数组
        for (int i = 1; i < m; i++) {               //通过递推规则计算
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m - 1][n - 1];
    }
}

一维记忆数组实现方式,由上图可知,要得到红圈的值,需要知道黑框的值,我们可以通过一个一维数组记录前一行的值,用一个数值记录前一列的值,然后每计算一次,更新数组的值,便能通过O(n)的空间来完成总路径的记录

dp[i] [j]=last+dp[i-1] [j];

class Solution {
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];  //一维记忆数组
        int last = 0;
        dp[0]=1;    //初始化记忆数组
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (j == 0) {
                    last = 0;   //第一列 从左边过来的路径为0
                }

                dp[j] = last + dp[j];   //其余列,从左边过来的路径加上一行路径之和
                last = dp[j];       //更新记忆数组
            }
        }
        return dp[n - 1];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值