动态规划问题
概念:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
动态规划法仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量,一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
通过leetcode相关题目来理解动态规划的套路
题目描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
解题思路:
1)dp[i] :存放数组前i个元素的最大子序列长度
2)初始化状态 dp[i] = 1; // 因为每个元素都可能是最大子序里,其长度为 1
3)对于 j 在 (0,i)区间,if num[i] > num[j],dp[i] = Math.max(dp[i], dp[j] + 1);否则 什么也不做
代码:
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length]; // 1)存放数组前i个元素的最大子序列长度
Arrays.fill(dp,1); // 2)确定动态规划状态
// 3)找出dp[i]中最大的数即为 最大子序列长度
int maxL = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j <= i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxL = Math.max(maxL, dp[i]);
}
return maxL;
}
复杂度分析:
- 时间复杂度:O(n^2)
- 空间复杂度:O(n)
题目描述:
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:
输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
解题思路:
1)dp[i] --- 存放数组前i个最大且连续的序列长度
2)初始化状态 dp[i] = 1; // 因为每个元素都可能是最大子序里,其长度为 1
3)对于i属于(1,nums.length),
if nums[i] > nums[i-1] 则 dp[i] = dp[i-1] + 1;
由于返回的是递增的最大长度,所以在申请一个临时变量保存数组中最大的元素(长度)返回即可。
代码:
public int findLengthOfLCIS(int[] nums) {
int[] dp = new int[nums.length]; // 1)存放数组前i个最大且连续的序列长度
Arrays.fill(dp, 1); // 2) 初始化状态 dp[i] = 1; // 因为每个元素都可能是最大子序里,其长度为 1
if (nums.length == 1) return 1;
int maxL = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] > nums[i - 1]) // 3)状态转移方程(根据题意,需要连续只需比较相邻两者即可
dp[i] = dp[i - 1] + 1;
maxL = Math.max(maxL, dp[i]);
}
return maxL;
}
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)
题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解题思路:
1)定义dp[i][j] --- 表示下标为i的字符串到下标为j的字符串是否为回文串
2)初始化,默认为false
3)算法核心思想:(采用暴力算法的优化解决)
3.1 如果 s[i] == s[j],且s[i+1,j-1]是回文串,则s[i][j]也是回文串
3.2 如果 s[i] == s[j],且i == j 指向同一元素,肯定是回文串
3.3 如果s[i] == s[j],且 j - i == 1,说明两者之间隔着单个元素,此时组成的字串也是回文串综上可将状态转移条件合并为如下语句:
if (s[i] == s[j] && (j-i<2 || dp[i+1][j-1]为回文串)) 则 s[i,j]也为回文串
代码:
public String longestPalindrome(String s) {
int n = s.length();
// 1)2)定义dp[i][j] --- 表示下标为i的字符串到下标为j的字符串是否为回文串,默认为false
boolean[][] dp = new boolean[n][n];
int maxLength = 0;
String maxStr = "";
// 3)状态转移方程
for (int j = 0; j < s.length(); ++j) {
for (int i = 0; i <= j; ++i) {
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i+1][j-1])) {
dp[i][j] = true;
if (maxLength < s.substring(i,j+1).length()) {
maxLength = s.substring(i,j+1).length();
maxStr = s.substring(i,j+1);
}
}
}
}
return maxStr;
}
复杂度分析:
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
题目描述:
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:
"cbbd"
输出:
2
解题思路:
注意这个跟最长回文子串不同之处在于 子序列,可能包括不连续的
1)定义dp[i][j]: 表示从第i个索引为止的元素到第j个元素的 子序列
2)初始化,如果只有一个字符,显然最长回文子序列长度是 1,也就是 dp[i][j] = 1 (i == j)
3)状态转移方程。 i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。
代码:
public int longestPalindromeSubseq(String s) {
int n = s.length();
// 1) 定义dp[i][j]: 表示从第i个索引为止的元素到第j个元素的 子序列
int[][] dp = new int[n][n];
// 如果只有一个字符,显然最长回文子序列长度是 1,也就是 dp[i][j] = 1 (i == j)
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。
for (int i = n - 1; i >= 0; --i) {
// dp[i][i] = 1;
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j))
dp[i][j] = dp[i+1][j-1] + 2; // 如果满足则在原子串上加2
else // 否则 取[i+1,j]与[i,j-1]两子串中的最大一个作为当前串[i,j]的长度
dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
}
}
// 整个 s 的最长回文子串长度
return dp[0][n-1];
}
复杂度分析:
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
动态规划模板步骤:
1)确定动态规划状态 (必选)
2)写出状态转移方程(画出状态转移表) (必选)
3)考虑初始化条件 (必选)
4)考虑输出状态 (必选)
5)考虑对时间,空间复杂度的优化(Bonus) (可选)
更多请见:https://github.com/datawhalechina/team-learning-program/blob/master/LeetCodeClassification/2.动态规划.md