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和其相邻的字符构成的字符串2也是回文字符串
- 定义记忆二维数组,如果dp[i+1] [j-1]&char[i]==char[j],则dp[i] [j]也是回文字符串
- 初始化数组,如果字符串长度为1,那么它们肯定是回文字符串,即dp[i] [i] = true,如果字符串的长度为2,那么只需要满足char[i]==char[j],则dp[i] [j]为回文字符串
- 应用递推公式求解
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];
}
}

465

被折叠的 条评论
为什么被折叠?



