0.前言
动态规划的步骤是什么?
1.找到状态、选择。状态,是在变化的东西;选择,是你可以选择的东西。
2.明确dp数组的含义
3.寻找状态转移方程
4.如何选择遍历方向?遍历时的2个注意事项:(1)、所需状态必须是已经计算出来的;(2)、遍历的终点必须是存储返回结果的位置。
1.最长递增子序列
1.题目分析
(1).明确dp[]数组的含义:dp[i]表示当数组以nums[i]结尾时,最长的递增子序列是多少;
(2).确定base case:dp[]数组初始化为1,因为就算只取nums[i]这1个元素,也至少是1;
(3).状态转移方程:j < i,当nums[j] < nums[i]时,dp[i] = max(dp[i], dp[j] + 1).
2.代码
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums==null || nums.length==0)
return 0;
int len = nums.length;
int res = 1;
1.定义动态数组
int[] dp = new int[len];
2.确定base case
Arrays.fill(dp, 1);
3.状态转移方程
for (int i = 0; i < len; i++) {
for (int j = 0; j < i; j++) {
当后比前大时
if (nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
else if (nums[i] <= nums[j]){
此时不更新dp[i]
}
动态获取最大值
res = Math.max(res, dp[i]);
}
}
return res;
}
}
2. 俄罗斯套娃信封问题
1.问题分析
- 信封嵌套要求什么?后1个信封的长、宽都要严格大于前1个信封。
(1).把信封排序,优先按长度升序排列,长度相同把高度降序排列;
(2).在高度序列的一维数组中,找出最长递增子序列,它的长度就是目标求的信封的长度; - 为什么要那样排序?
因为降序排列的高度,在取最长递增子序列时,可以保证相同长度的信封只取到1个。长度相同的信封中,最高的那个排在最前边,因为是取高度的最长递增子序列,所以他后边的不会被取到。
2.代码
class Solution {
public int maxEnvelopes(int[][] envelopes) {
先按第1个元素升序排列,前者相同则按第2个元素降序排列
Arrays.sort(envelopes, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
对第2个元素降序排列 对第1个元素升序排列
return o1[0] == o2[0] ? o2[1] - o1[1] : o1[0] - o2[0];
}
});
int len = envelopes.length;
int[] width = new int[len];
for (int i = 0; i < len; i++) {
width[i] = envelopes[i][1];
}
调用上1个函数,求最长递增子序列
return lengthOfLIS(width);
}
}
3.剑指 Offer 42. 连续子数组的最大和
1.题目分析
- 1.动态数组dp[],dp[i]表示nums[i]及其之前的最大子数组和是多少;
- 2.base case,dp[0] = nums[0];
- 3.状态转移方程,从前向后遍历,dp[i] = Math.max(0,dp[i-1]) + nums[i]。
2.代码
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
1.定义dp数组, dp[i]表示nums[i]及其之前的最大子数组和是多少?
int[] dp = new int[len];
2.base case 先跳过
dp[0] = nums[0];
int res = nums[0];
3.状态方程
for (int i = 1; i < len; i++) {
如果第i个元素之前的和比0都小,那直接不要了
dp[i] = Math.max(0,dp[i-1]) + nums[i];
动态更新res返回值
res = Math.max(res, dp[i]);
}
return res;
}
}
4.剑指 Offer II 095. 最长公共子序列
1.题目分析
-
1.dp[][]数组: dp[i][j]表示 text1前i个字符(相当于到nums[i-1]处) 与 text2前j个字符(相当于到nums[j-1]处) 最长公共子序列是多少
-
2.base case: dp[][0]==0 表示text1 与text2空字符串 没公共部分;
dp[0][]==0 表示text2 与text1空字符串 没公共部分。 -
3.正向遍历,状态转移方程: if(text1[i-1] == text2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else if(text1[i-1] != text2[j-1]) dp[i][j] = max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])。
2.代码
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if (text1==null || text2==null)
return 0;
int olen = text1.length();
int tlen = text2.length();
1.定义dp数组, 各自+1,是为了最前边第0行、0列的空串的情况
dp[i][j]表示 text1前i个字符 与 text2前j个字符最长公共子序列是多少
int[][] dp = new int[olen+1][tlen+1];
2.base case 反正是0,不必处理
3.状态转移方程 从左上角开始正向遍历
for (int i = 1; i < olen+1; i++) {
for (int j = 1; j < tlen+1; j++) {
if (text1.charAt(i-1) == text2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
}
else if (text1.charAt(i-1) != text2.charAt(j-1)) {
dp[i][j] = Math.max(Math.max(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1]);
}
}
}
return dp[olen][tlen];
}
}
5. 编辑距离
1.题目分析
-
1.dp[]数组,dp[i][j] 表示word1前i的字符 变为 word2前j个字符 所需最少步数;
-
2.base case:
- dp[i][0] = i, word2空串 变为 word1所需的最少步数;
- dp[0][j] = j,word1空串 变为 word2所需的最少步数;
-
3.正向遍历,状态转移方程,word1 转变为 word2 时 各个字符有 4 种操作
- 1.跳过 dp[i-1][j-1] --(步数+0)–> dp[i][j]
- 2.删除 dp[i-1][j] --(步数+1)–> dp[i][j]
- 3.增加 dp[i][j-1] --(步数+1)–> dp[i][j]
- 4.替换 dp[i-1][j-1] --(步数+1)–> dp[i][j]
2.代码
class Solution {
public int minDistance(String word1, String word2) {
int oLen = word1.length();
int tLen = word2.length();
1.dp[]数组,dp[i][j] 表示word1前i的字符 变为 word2前j个字符 所需最少步数
int[][] dp = new int[oLen+1][tLen+1];
2.base case
word2空串 变为 word1所需的最少步数
for (int i = 0; i < oLen+1; i++) {
dp[i][0] = i;
}
word1空串 变为 word2所需的最少步数
for (int i = 0; i < tLen+1; i++) {
dp[0][i] = i;
}
3.状态转移方程,有:跳、增、删、替换 4种操作
for (int i = 1; i < oLen+1; i++) {
for (int j = 1; j < tLen+1; j++) {
无需操作,直接跳过
if (word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
}
需要操作,步数+1; 所需的状态皆已求出
else {
dp[i][j] = 1 + minThree(
dp[i-1][j], 删除 1个元素
dp[i-1][j-1], 替换 1个元素
dp[i][j-1]); 增加 1个元素
}
}
}
return dp[oLen][tLen];
}
int minThree(int i, int j, int k) {
return Math.min(Math.min(i,j),k);
}
}
6. 最长回文子序列
1.题目分析
-
1.dp数组,dp[i][j] 表示 s[i] -到-> s[j] 之间的最长回文子序列的长度
-
2.base case
- 当i==j 时,则为1
- 当 i > j 时,则为0
- 当 i < j 时,则为需要计算的情况
-
3.状态转移方程,反向遍历
- 出现回文字符时: if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1] + 2
- 没有回文字符时 else dp[i][j] = Max( dp[i][j-1], dp[i+1][j] )
-
4.目标是求dp[0][n-1]
2.代码
class Solution {
public int longestPalindromeSubseq(String s) {
if (s == null || s.length() == 0)
return 0;
int len = s.length();
1.建立动态 dp
int[][] dp = new int[len][len];
2.base case
for (int i = 0; i < len; i++) {
dp[i][i] = 1;
}
3.状态转移方程 反向遍历,保证所需状态都是已经计算出来的
for (int i = len-2; i >= 0; i--) {
for (int j = i+1; j < len; j++) {
if (s.charAt(i) == s.charAt(j))
dp[i][j] = dp[i+1][j-1] + 2;
else{
dp[i][j] = Math.max(dp[i][j-1], dp[i+1][j]);
}
}
}
return dp[0][len-1];
}
}