文章目录
1. 回溯法
经典问题:排列、组合、切割、子集、棋盘
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
1.1 组合
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>(); //记录返回的结果
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
public void backtracking(int n, int k, int startIndex){
// 终止条件
if(path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
// for 循环处理每个节点,进行递归
for(int i = startIndex; i <= n; i++){
path.add(i);
backtracking(n, k, i+1);
// 回溯
path.removeLast();
}
}
}
剪枝操作:
已经选择的元素个数:path.size();
还需要的元素个数为: k - path.size();
在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
1.2 组合总和3
思路:
树的深度(递归层数):k
树的宽度(for循环次数):9
每一层中,定义处理逻辑
class Solution {
List<List<Integer>> result = new ArrayList<>(); // 记录返回的结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracing(n, k, 1, 0);
return result;
}
public void backTracing(int targetSum, int k, int startIndex, int sum){
// 剪枝
if (sum > targetSum){
return;
}
if (path.size() == k) {
if (sum == targetSum) {
result.add(new ArrayList<>(path));
}
return;
}
// 递归,并剪枝
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
path.add(i);
sum += i;
backTracing(targetSum, k, i+1, sum);
// 回溯
path.removeLast();
sum -= i;
}
}
}
1.3 电话号码的字母组合
思路:输入两个数字两个for循环、3个数字三个for循环,输入n个数字用回溯,比如输入两个数字1和2,根节点先在1对应的字母里面取:for循环三次,每次取一个字母,然后递归取数字2对应的字母。下层就是在数字2的基础上,for循环,每次取一个数,递归下层,以此类推。
树的深度(递归层数) = 输入的数字个数
树的宽度(for循环次数) = 3
class Solution {
//设置全局列表存储最后的结果
List<String> result = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) {
return result;
}
// 初始化对应的数字:为了直接对应2-9,新增加了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return result;
}
//每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
StringBuilder temp = new StringBuilder();
// 根节点递归的是第一个数字,下一层节点递归的是第二个数字,以此类推
// index 记录了当前是第几个数字
public void backTracking(String digits,String[] numString, int index) {
// 叶子节点收集,举例:index = 2,digits.length = 2,此时0和1位置数字处理完,现在是索引为2,可以收集了
if(index == digits.length()) {
result.add(temp.toString());
return;
}
// str表示当前index对应的字符串
String str = numString[digits.charAt(index) - '0'];// 举例 :'5' -> 5
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
// 递归处理 下一层
backTracking(digits, numString, index+1);
// 回溯,
temp.deleteCharAt(temp.length() - 1);
}
}
}
1.4 组合总和
思路:可以重复选取元素,选取了2下一层还可以包括2;因为是组合,使用startIndex,startIndex记录for的这个元素的下层在哪个起始位置开始找。
树深:没有限制,只要sum大于target就return
树宽:for循环的次数,根据startIndex来定
class Solution {
List<Integer> path = new ArrayList<>(); // 记录选取的元素,回溯使用
List<List<Integer>> result = new ArrayList<>(); // 记录结果
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracing(candidates, target, 0, 0);
return result;
}
public void backTracing(int[] candidates, int targetSum, int sum, int startIndex) {
// 递归结束条件
if(sum > targetSum) {
return;
}
// 找到了数字和为target的组合
if(sum == targetSum) {
result.add(new ArrayList<>(path));
}
for(int i = startIndex; i < candidates.length; i++) {
path.add(candidates[i]);
sum += candidates[i];
backTracing(candidates, targetSum, sum, i);
path.removeLast();
sum -= candidates[i];
}
}
}
1.5 切割回文串
思路:根节点拿到字符串,for循环遍历每个字符,【startIndex,i】表示当前切割的区间,判断这个区间是不是回文,是的话才递归判断下一层,传入下一层的参数startIndex+1,这个和组合问题像,当前这个位置已经切割了那下一层递归切割就得在这个位置后面切割,不能重复切割当前元素。
class Solution {
List<List<String>> result = new ArrayList<>();
List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
backTracing(s, 0);
return result;
}
public void backTracing(String s, int startIndex) {
// 如果起始位置大于 s 的大小,说明找到了一组分隔方案
if(startIndex >= s.length()) {
result.add(new ArrayList<>(path));
return;
}
for(int i = startIndex; i < s.length(); i++) {
// 如果是回文串,则记录
if(isPalindrome(s, startIndex, i)) {
path.add(s.substring(startIndex, i+1));
}else {
continue;
}
// 只有取的是回文串才进入下层递归
// 起始位置后移,保证不重复
backTracing(s, i+1);
path.removeLast();
}
}
// 判断回文串
private boolean isPalindrome(String s, int startIndex, int end) {
for (int i = startIndex, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}
1.6 子集
思路:组合和分隔问题都是在叶子节点上收集结果,子集问题是在每个节点上收集结果,没进入一层递归都要收集结果:根节点收集空集、下一层收集一个元素的集合、下下层收集两个元素的集合,以此类推,,每一层递归的startIndex是当前元素 i + 1,递归终止的条件是startIndex走到集合的末尾。
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracing(nums,0);
return result;
}
private void backTracing(int[] nums, int startIndex) {
// 每个节点都收集,这个是区别于 组合
result.add(new ArrayList<>(path));
if(startIndex >= nums.length) {
return;
}
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backTracing(nums, i + 1);
path.removeLast();
}
}
}
1.7 全排列
思路:全排列和组合的区别就是:组合用startIndex控制别取前面的元素,而排列问题可以取前面的元素,用used【】数组来控制可以取前面的元素。也是都在叶子节点收集结果,只有子集是在每个节点上收集。
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
List<Integer> path = new ArrayList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if(nums.length == 0) {
return result;
}
used = new boolean[nums.length];
backTracing(nums);
return result;
}
private void backTracing(int[] nums) {
// 叶子节点收集结果
if(path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for(int i = 0; i < nums.length; i++) {
// 从根到叶子的路径中,如果元素取过,则不处理这个元素
if(used[i]) {
continue;
}
used[i] = true;
path.add(nums[i]);
backTracing(nums);
// 回溯
used[i] = false;
path.removeLast();
}
}
}
2. 贪心算法
2.1 分发饼干
思路:
-
为了满足更多的小孩,就不要造成饼干尺寸的浪费。
-
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
-
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
2.2 买卖股票的最佳时机2
思路:最终利润是可以分解的:假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
局部最优:收集每天的正利润,全局最优:求得最大利润。
2.3 跳跃游戏
思路:根据跳跃的覆盖范围,看看能不能覆盖整个数组
- 遍历覆盖范围内的元素,更新覆盖范围
2.4 跳跃游戏2
思路:题目要计算最少的步数,那么就要想清楚什么时候步数才一定要加一呢?
贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。这里的多走是指覆盖范围最大,而不是跳最大的步数。实现:在当前的覆盖范围内,看一下下一步的覆盖范围,如果下一步的覆盖范围可以覆盖整个数组,那么步数 + 1并退出,如果下一步的覆盖范围没有覆盖整个数组,说明当前区域的覆盖范围还不够,此时步数 + 1再从下一步的覆盖范围依照上面的判断。
class Solution {
public int jump(int[] nums) {
if (nums == null || nums.length == 0 || nums.length == 1) {
return 0;
}
int count = 0; // 记录跳跃的次数
int curDistance = 0; // 当前最大的覆盖范围
int nextDistance = 0; // 下一次的最大覆盖范围
for(int i = 0; i < nums.length; i++) {
// 在可覆盖的区域范围内更新下一次的最大覆盖范围
nextDistance = Math.max(nextDistance, i + nums[i]);
// 当下一次覆盖范围可以覆盖整个数组时说明再走一步就ok
if (nextDistance >= nums.length - 1) {
count++;
break;
}
// 当前区域范围走完时,下一次覆盖范围不能包括整个数组,此时更新当前区域,步数 +1
if (i == curDistance) {
curDistance = nextDistance;
count++;
}
}
return count;
}
}
2.5 划分字母区间
思路:一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。统计每个字母出现的最远位置,在选择划分区间时,定义左右指针指向区间两端,每遍历一个元素更新区间的右指针,当右指针和当前遍历的位置重合时,表示找到一个区间,记录,开始下一个区间。
class Solution {
public List<Integer> partitionLabels(String s) {
// 思路:统计每个元素的最远位置,再遍历字符串,确定区间的左右边界
List<Integer> result = new LinkedList<>();
int[] edge = new int[26]; // 记录字母的最远边界
char[] chars = s.toCharArray();
// 统计字母的最远边界
for (int i = 0; i < chars.length; i++) {
edge[chars[i] - 'a'] = i;
}
int left = 0;
int right = 0;
for (int i = 0; i < chars.length; i++) {
right = Math.max(edge[chars[i] - 'a'], right);
if (right == i) {
result.add(right - left + 1);
left = right + 1;
}
}
return result;
}
}
3. 动态规划
3.1 使用最小花费爬楼梯
思路:就是爬楼梯,只不过爬是需要代价的,求怕到顶楼的最小代价。
递归五部曲:
- 确定dp数组以及下标的含义:dp【i】表示到达第 i 台阶所花费的最少体力为dp【i】。
- 确定递推公式:dp【i】= min(dp【i - 1】+ cost【i - 1】,dp【i - 2】+cost【i - 2】)
- dp数组初始化:题目描述起始可以选择在0或1位置,dp【0】 = 0,dp【1】 = 0
- 确定遍历顺序:前往后
class Solution {
public int minCostClimbingStairs(int[] cost) {
// dp【i】表示到达第 i 个台阶的最小花费
int len = cost.length;
int[] dp = new int[len + 1];
dp[0] = 0;
dp[1] = 0;
// 计算到达每一层的最小花费
for(int i = 2; i <= len; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[len];
}
}
3.2 不同路径
思路:题目问的是不同的路径,走到(i,j)位置只能有上方和左面得到,走到(i,j)位置路径数是上方和左面路径数之和。
走到上方的不同路径数量有 n 种,再往下走一步到达(i, j)位置,路径数是没有变的,都是同一条路径,不是求的步数,不用 + 1。
递归五部曲:
- 确定dp数组下标含义 dp[i][j] 到每一个坐标可能的路径种类
- 递推公式 dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 初始化 dp[i][0]=1 dp[0][i]=1 初始化横竖就可
- 遍历顺序 一行一行遍历
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化
for (int i = 0; i< m; i++) {
dp[i][0] = 1;
}
for (int i = 0; i< n; i++) {
dp[0][i] = 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];
}
}
3.3 不同路径2
思路:比不同路径来说,多了障碍。
- 在dp数组推导时:有障碍的位置dp数组为0,表示到达这个位置的路径为0没有路径可到达,当前位置不是障碍才推到。
- 在初始化时:第一行和第一列本来要初始化为1,但是可能期间有障碍,在有障碍的地方及障碍后面都初始化dp数组为0
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
// 如果在起点或者终点出现了障碍,直接返回 0
if(obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) {
return 0;
}
// 初始化:在出现障碍位置及以后位置不赋值,默认为 0 ,表示该位置不可达
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}
for (int i = 1; i < m; i ++) {
for (int j = 1; j < n; j++) {
dp[i][j] = (obstacleGrid[i][j] == 0) ? dp[i - 1][j] + dp[i][j - 1] : 0;
}
}
return dp[m - 1][n -1];
}
}
3.4 背包问题
0-1背包:
- dp定义:即dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 递推:有两个方向推出来dp[i][j]
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
- 所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- 初始化:由递推公式,初始化第一行和第一列
- 遍历顺序
3.5 分割等和子集(0-1背包)
思路:算出数组的sum,除以二为target,target就是背包的容量,在数组里面填满背包容量为target的最大价值,要让最大价值也为target,所以数组中元素的值既是容量又是价值。
动规五部曲:
- 定义:dp【j】表示背包总容量为 j 的最大价值
- 确定递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
- dp数组如何初始化:dp[0]一定是0(如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。)
- 确定遍历顺序:先遍历物品,再遍历背包,背包逆序
class Solution {
public boolean canPartition(int[] nums) {
// 0-1背包问题
if (nums == null || nums.length == 0) return false;
int n = nums.length;
int sum = 0;
for (int num : nums) {
sum += num;
}
// 总和为奇数,不能平分
if (sum % 2 != 0) return false;
// 0-1 背包
int target = sum / 2;
int[] dp = new int[target + 1];
for (int i = 0; i < n; i++) {
for (int j = target; j >= nums[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
}
3.6 零钱兑换(完全背包)
思路:完全背吧问题
动规五部曲:
- 定义:dp[j]表示凑足总额为j所需钱币的最少个数为dp[j]
- 确定递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);,当前 i 位置硬币加不加,不加的话凑足总额为 j 的背包的最小硬币个数还是为dp[j],如果当前 i 位置硬币加的话,凑足总额为 j 的背包的最小硬币个数dp【j】=dp【j - coins【i】】 + 1
- dp数组如何初始化:dp【0】=0;递推公式是求min,所以,初始化其余位置为int的最大值
- 遍历顺序:钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。(题外话:组合是先遍历物品再遍历背包,排列则相反)
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
// dp【j】定义为装满容量为 j 的背包的最小硬币个数为dp【j】
int[] dp = new int[amount + 1];
// 初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
// 当金额为 0 时需要的硬币数为0
dp[0] = 0;
// 求的是个数,和组合排列没关系,for谁先都ok
for (int i = 0; i < coins.length; i++){
// 完全背包,正序遍历
for (int j = coins[i]; j <= amount; j++) {
// 只有dp[j - coins[i]] 不是初始最大值时,该位才有选择的必要。
if (dp[j - coins[i]] != max) {
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == max ? -1 : dp[amount];
}
}
3.7 完全平方数
思路:和零钱兑换思路差不多,物品就是各个平方数,要装满背包最少需要多少个物品。
动规五部曲:
- dp定义:dp【j】表示装满容量为 j 的背包最少需要的平方数为dp【j】个
- 递推公式:dp【j】 = min(dp【j】,dp【j - i*i】 + 1)
- dp数组初始化:dp【0】为0,其余位置int的最大值
- 遍历顺序:都可以
class Solution {
public int numSquares(int n) {
// 完全背包问题
int max = Integer.MAX_VALUE;
int[] dp = new int[n + 1];
// 初始化
for (int i = 0; i < dp.length; i++) {
dp[i] = max;
}
dp[0] = 0;
for (int i = 1; i*i <= n; i++) {
// 完全背包问题用正序
for (int j = i*i; j <= n; j++) {
if (dp[j - i*i] != max) {
dp[j] = Math.min(dp[j], dp[j - i*i] + 1);
}
}
}
return dp[n];
}
}
3.8 单词拆分
思路:题目要求字段中字符串可否组成给定的target字符串,可以理解为背包问题:给定物品,看能否装满背包,物品可以重复,则是完全背包问题(正序遍历背包),组成的target字符串是有顺序区别的,必须先leet再code,则是排列问题(先遍历背包,再遍历物品)。
动规五部曲:
- dp定义:字符串长度为 i 的话,dp[ i ]为true,表示可以拆分为一个或多个在字典中出现的单词
- 递推公式:如果确定dp[ j ] 是true,且 [ j, i ] 这个区间的子串出现在字典里,那么dp[ i ]一定是true。(j < i )。所以递推公式是 if([ j, i ] 这个区间的子串出现在字典里 && dp[ j ]是true) 那么 dp[ i ] = true。
- dp数组初始化:dp[0]初始为true完全就是为了推导公式。下标非0的dp[i]初始化为false
- 遍历顺序:因为是排列,先背包再物品
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// 完全背包并且是排列情况
HashSet<String> set = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
// 先背包再物品,并且背包正序
for (int i = 1; i <= s.length(); i++) {
// 这里物品指的是[j,i)的字串,注意:取的物品是从索引为 0 开始
for (int j = 0; j < i; j++) {
if (set.contains(s.substring(j, i)) && dp[j]) {
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
3.9 打家劫舍
思路:定义dp数组,当前元素偷与不偷两个状态来推出递推公式。
动规五部曲:
- dp定义:dp[ i ]:考虑下标 i(包括 i)以内的房屋,最多可以偷窃的金额为dp[ i ]。
- 递推公式:决定dp[ i ]的因素就是第 i 房间偷还是不偷。如果偷第 i 房间,那么dp[ i ] = dp[i - 2] + nums[ i ] ,即:第 i-1 房一定是不考虑的,如果不偷第i房间,那么dp[ i ] = dp[i - 1],即考 虑 i-1 房,递推公式为:dp[ i ] = max(dp[ i - 2 ] + nums[ i ], dp[ i - 1 ]);
- dp初始化:dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即
- 遍历顺序:左到右
class Solution {
public int rob(int[] nums) {
if(nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
// dp定义:考虑第 i 个房(包括第 i 个)所能偷的最大值
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[nums.length - 1];
}
}
3.10 最长递增子序列(子序列问题)
思路:子序列问题,定义dp【i】表示以 i 位置元素 结尾的的最长递增子序列的长度,那dp【i】怎么更新呢 ?就要遍历 i 之前的元素,来更新dp【i】,最后dp中存的是以 每个元素结尾的最长递增子序列,结果不是取dp【length - 1】,而是得遍历dp,找最大值,因为不一定以结尾得元素是最长递增子序列,可能在其他的位置上。
动规五部曲:
- dp定义:dp[ i ]表示 i 之前包括 i 的以nums[ i ]结尾的最长递增子序列的长度
- 递推公式:位置 i 的最长递增子序列等于 j 从0到 i-1 各个位置的最长升序子序列 + 1 的最大值。所以:if (nums[ i ] > nums[ j ]) dp[ i ] = max(dp[ i ], dp[ j ] + 1);
- dp初始化:每一个i,对应的dp[ i ](即最长递增子序列)起始大小至少都是1
- 遍历顺序:dp[ i ] 是有0到 i-1 各个位置的最长递增子序列 推导而来,那么遍历 i 一定是从前向后遍历。
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length <= 1) return nums.length;
// dp【i】: 以 i位置元素 结尾的最长递增子序列为dp【i】
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
// i=0位置元素没必要遍历,从 i = 1 开始
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
// 只有当i位置元素大于j位置元素时,才更新dp
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
int result = 0;
for (int i = 0; i < dp.length; i++){
result = Math.max(result, dp[i]);
}
return result;
}
}
3.11 最长公共子序列(二维dp)
思路:比较两个数组的最长公共子序列,要根据两个数组的每个元素状态来推导,用二维dp。用二维dp要注意dp的定义,初始化
动规五部曲:
- dp定义:dp[ i ][ j ]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[ i ][ j ]。要注意定义,这样定义是为了初始化方便
- 递推公式:主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同。如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[ i ][ j ] = dp[i - 1][j - 1] + 1;如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[ i ][ j ] = max(dp[i - 1][ j ], dp[ i ][j - 1]);
- dp初始化:第一行第一列初始化为 0
- 遍历顺序:左到右
注意:dp的定义,dp[ i ][ j ]表示长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列,最后求得是dp【text1.length】【text2.length】才表示text1与text2的最长公共子序列
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] char1 = text1.toCharArray();
char[] char2 = text2.toCharArray();
// 二维dp,要注意dp的定义
// dp【i】【j】: 表示0到i-1的text1和0到j-1的text2的最长公共子序列
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
for (int i = 1; i <= text1.length(); i++) {
for(int j = 1; j <= text2.length(); j++) {
if(char1[i - 1] == char2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.length()][text2.length()];
}
}