算法刷题:DP专题(9.16,持续更)
算法刷题系列上期:
力扣链接:
递归中的状态转移方程:动态规划题目:
目录
动态规划基础
递归 / 多阶段最优问题
多个阶段操作后的最优问题,如果可以转化为根据新阶段的操作结果 累加到对应旧阶段操作 累计 的结果,这是典型的递归思路,也是状态转移
子问题和状态
状态就是子问题的解
具体的说‘阶段’概念
数据容器 下一个迭代位置
- 打家劫舍 的房子,就是数组;
- LCS 和 LRA 的 s1、s2 就是数组 / 串;
- 最长有效括号 的载体也是数组 / 串;
- 通配符匹配 的下一个阶段也是数组 / 串的 最新索引位置
等等...
状态和阶段的区别
- 状态是从一组元素中计算的结果;
- 阶段是从一个元素中计算;
状态转移方程
i - 阶段下 - 决策的定义域、f - 决策 / 问题处理 函数、f(i) - 状态
\[f(i) = \begin{cases} g(f(k)),\,\,条件1\\ g(f(t)),\,\,条件2\\ ....\\ \end{cases} \]
意义
- 递归树的剪枝
- 递归子问题/层层决策 的 全局最优解,避免贪心的鼠目寸光
- 空间换时间,提高时间复杂度
最后说说解题思路
- 找出 单阶段的 操作 和 条件判断
- 找出 原问题 / 子问题 / 状态
- 确认 单个阶段的操作(1个) 和 子问题 / 旧状态(1组) 的关系
- 列出 状态转移方程
递归中的状态转移方程
最长同值路径(medium)
列出状态转移方程
当前状态作为原问题 需解决的 / 递 的子问题、向上层贡献 / 归:
\(f(i)=1 + (i.l.val==i.val?f(i.l):0) + (i.r.val==i.val?f(i.r)?0)\)
当前状态作为 原问题本身,看看能不能全局最优:
\(f(i)=1 + max((i.l.val==i.val?f(i.l):0),\,\,(i.r.val==i.val?f(i.r)?0))\)
代码实现(3ms)
class Solution {
int ans = 0;
public int longestUnivaluePath(TreeNode root) {
if(root == null) return 0;
dfs(root);
return ans - 1; // -1 是把 节点数 表示为 边数
}
int dfs(TreeNode root){
if(root == null){
return 0;
}
int lft = dfs(root.left);
int rgt = dfs(root.right);
int res = 1;
// 假设作为根节点 / 全局最优
if(root.left != null){
if(root.left.val == root.val){
res += lft;
} else lft = 0;
}
if(root.right != null){
if(root.right.val == root.val){
res += rgt;
} else rgt = 0;
} if(ans < res) ans = res;
// 假设作为向上层的贡献
// 告诉上层,以我为起点的同值路径长度是多少
return 1 + Math.max(lft, rgt);
}
}
二叉树中的最大路径和(hard)
列出状态转移方程
作为原问题的子树、子问题的解(返回给顶层):
\(f(x)=x.val + max(0,\,\,f(x.left),\,\,f(x.right))\)
作为原问题,看当前局部 是否是 全局最大值:
\(f(x)=x.val + max(0,\,\,f(x.left),\,\,f(x.right),\,\, f(x.left)+f(x.right))\)
代码实现 (0ms)
class Solution {
int ans = -0x7fffffff; // 最大值记录
public int maxPathSum(TreeNode root) {
f(root);
return ans;
}
int f(TreeNode root){
if(root == null) return 0;
int lft = f(root.left);
int rgt = f(root.right);
// 后序位置
// 路径的几种情况
int a = root.val;
int b = root.val + lft;
int c = root.val + rgt;
int d = root.val + lft + rgt;
// 作为根节点,尝试 更新 最大值的记录ans
int curmax = Math.max(Math.max(a, b), Math.max(c, d));
if(curmax > ans) ans = curmax;
// 作为子节点 贡献上层
int retmax = Math.max(Math.max(a, b), c);
return retmax;
}
}
基础题目
爬楼梯(easy)
思路分析
- 当前阶段?到 这个位置,从 \((i-2)\) 或 \((i-1)\) 的台阶到 \(i\) 位置
- 一组阶段?如何从 \(0\) 位置到 \((i-2)\) / \((i-1)\)
- 状态转移方程:略
代码实现:
class Solution {
public int climbStairs(int n) {
int [] f = new int [n + 1];
f[0] = 1, f[1] = 1;
for(int i = 2; i <= n; i++)
f[i] = f[i - 1] + f[i - 2];
return f[n];
}
}
三角形最小路径和(medium)
思路分析
- 问题的子结构
- 原问题:从尖端到三角形最下层的最短路径
- 子问题 1:从尖端到最下层第 j 节点的最短路径
- 子问题 1-1:从尖端到第 i 层第 j 节点的最短路径
- 定义域:\((i,\,j)\)
- 状态转移方程:
\[f(i, j)= \begin{cases} f(i-1, j) + ta[i][j],\,\,j = 0\\ min(f(i-1,j-1),f(i-1,j))\,+\,ta[i][j],\,\,0 < i < 上层边界\\ f(i-1, j-1) + ta[i][j],\,j=上层边界\\ \end{cases} \]
代码实现(3ms)
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if(triangle == null || triangle.isEmpty()) return 0;
int n = triangle.size();
int [][] f = new int [n][n];
// ift0
f[0][0] = triangle.get(0).get(0);
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
int lf = 0x3fffffff, rf = f[i - 1][j];
if(j > 0) lf = f[i - 1][j - 1];
// 最开始是 f[i][j] = lf < rf ? lf : rf; //
f[i][j] = (lf < rf ? lf : rf) + triangle.get(i).get(j);
} // f[i][j] = f[i - 1][j - 1];
// 最开始忘记 triangle.get(i).get(j) 这部分了
f[i][i] = f[i - 1][i - 1] + triangle.get(i).get(i);
}
int min = 0x3fffffff;
for(int i = 0; i < n; i++){
if(min > f[n - 1][i]){
min = f[n - 1][i];
}
} return min;
}
}
打家劫舍(medium)
思路分析
代码实现(0ms)
class Solution {
public int rob(int[] u) {
if(u.length == 0) return 0;
if(u.length == 1) return u[0];
int [] f = new int[u.length];
f[0] = u[0], f[1] = Math.max(u[0], u[1]);
for(int i = 2; i < f.length; i++)
f[i] = f[i - 2] + u[i] > f[i - 1] ? f[i - 2] + u[i] : f[i - 1];
return f[f.length - 1];
}
}
最长重复子数组(medium)
思路分析
- 问题子结构:
- 原问题:求 s1 前 n 项和 s2 前 m 项进行匹配,找到最长重复子串
- 子问题:求 s1 前 i 项和 s2 前 m 项进行匹配,找到最长重复子串
- 定义域:\(i\) 表示 \(a\) 串的尾端位置,\(j\) 表示 \(b\) 串的尾端位置
- 状态值:\(f(i,j)\) 表示 a[i] 和 b[j] 匹配时,最后位置(\((i-1,j-1)\))已经匹配到的子串长度
- 状态前进:\((i-1, j-1) -> (i, j)\)
- 状态转移方程:
\[f(i, j)= \begin{cases} f(i-1,j-1)+1,\,\,a[i] = b[j]\\ 0,\,\,\,\,\,\,\,\,a[i] \neq b[j]\\ \end{cases} \]
代码实现(31ms)
class Solution {
public int findLength(int [] s1, int [] s2) {
int n1 = s1.length, n2 = s2.length;
int [][] f = new int[n1][n2];
// 初始值
int ans = 0;
for(int i = 0; i < n1; i++){
if(s1[i] == s2[0]){
f[i][0] = 1;
if(ans < 1) ans = 1;
}
}
for(int j = 0; j < n2; j++){
if(s1[0] == s2[j]){
f[0][j] = 1;
if(ans < 1) ans = 1;
}
}
// i, j 遍历 状态定义域
for(int i = 1; i < n1; i++){
for(int j = 1; j < n2; j++){
// 状态转移
f[i][j] = g(f[i - 1][j - 1], s1[i], s2[j]);
// f[i][j] = f[i - 1][j - 1] + g(s1[i], s2[j]);
if(f[i][j] > ans) ans = f[i][j];
}
}
return ans;
}
int g(int std, int c1, int c2){
return c1 == c2 ? std + 1 : 0;
}
}
LCS / 最长公共子序列(medium)
思路分析
- 问题的子结构:
- 原问题:s1 的前 n 项和 s2 的前 m 项已经匹配到的最长子序列
- 子问题:s1 的 i 前缀和 s2 的 j 前缀已经匹配到的最长子序列
- 定义域:\((i,\,j)\)
- 进入下一阶段时的情况:
- 如果 \(a[i]=b[j]\) ,那么当前阶段要在上一阶段状态的基础上
符合条件,继承上个状态的积累,并加上 新的一个单位的积累 - 如果 \(a[i]\neq b[j]\) ,那么要找到 \(a[i]、b[j]\) 不同时出现的情况的最大边界;
不符合条件,继承 最近最多 状态的积累,只继承
- 如果 \(a[i]=b[j]\) ,那么当前阶段要在上一阶段状态的基础上
- 状态转移方程:
\[f(i, j) = \begin{cases} f(i-1, j-1) + 1,\,\,\,\,a[i] = a[j]\\ max(f(i-1, j),\,f(i, j-1))\,,a[i] \neq b[j]\\ \end{cases} \]
代码实现(15ms)
class Solution {
public int longestCommonSubsequence(String s1, String s2) {
int n1 = s1.length(), n2 = s2.length();
int [][] f = new int [n1 + 1][n2 + 1];
for(int i = 1; i <= n1; i++){
char c1 = s1.charAt(i - 1);
for(int j = 1; j <= n2; j++){
char c2 = s2.charAt(j - 1);
if(c1 == c2)
f[i][j] = f[i - 1][j - 1] + 1;
else
f[i][j] = Math.max(f[i][j - 1], f[i - 1][j]);
}
} return f[n1][n2];
}
}
LIS / 最长递增子序列
思路分析
代码实现
class Solution {
public int lengthOfLIS(int[] s) {
if(s.length == 0) return 0;
int maxans = 1;
int [] f = new int [s.length + 1];
f[1] = 1;
for(int i = 2; i < f.length; i++){
f[i] = 1;
for(int j = 1; j < i; j++){
// f[i] = Math.max(f[j], f[i]);是不行的
// 反例:[1, 3, 6, 7, 9, ** 4 ** , 10]
// 而且每个状态 / f[j] 维护的是以 s[j] 为结尾的最长递增序列
// 所以 f[-1] 并不一定是全局的最长递增序列
// 但是所有的最长递增子序列 一定在 f 中
// 所以最后遍历一遍 f
if(s[j - 1] < s[i - 1]){
f[i] = Math.max(f[j] + 1, f[i]);
} // if(f[i] > maxans) maxans = f[i];
}
}
for(int ans : f){
if(ans > maxans) maxans = ans;
} return maxans;
}
}
最长有效括号(hard)
所求问题无后效性,就一定作为状态吗?
所求问题可能不好维护;
- 比如求一段区间内符合条件的最长子串,如果这段区间里有很多段子串,随着区间的扩大,维护多个子串的代价很大
- 所以不能因为所求问题是无后效性的,就将其作为状态维护起来,可能需要简化问题,让状态尽可能单一
思路分析
- 问题的子结构:
- 所求问题:\([0,\,n)\) 区间上的最长子串
- 简化后的原问题:\([0,\,n)\) 区间上,以 \(n-1\) 位置为尾端的子串的长度
- 子问题:
- 定义域:\(i\,\epsilon\,[0,\,n)\)
- 状态转移方程:
\[f(i)= \begin{cases} 0,\,\,s[i]="("\\ s[i]=")" \begin{cases} f(i-2)+2,\,\,\,\,s[i-1] ="("\\ f(i-1)+f(i-f(i-1)-2)+2,\,\, \begin{cases} s[i-1]=")"\\ s[i-f(i-1)-1]="("\\ \end{cases} \end{cases} \end{cases} \]
代码实现(1ms击败100%)
class Solution {
public int longestValidParentheses(String s) {
if(s.length() < 2) return 0;
char [] cs = s.toCharArray();
int [] f = new int [cs.length];
int ans = 0;
for(int i = 1; i < cs.length; i++){
// if(cs[i] == '(')
// f[i] = 0;
if(cs[i] == ')') {
if(cs[i-1] == '(')
f[i] = (i > 1 ? f[i-2] : 0) + 2;
else { // cs[i-1] == ')'
int idx = i - f[i-1] - 1;
if(idx >= 0 && cs[idx] == '(')
f[i] = f[i - 1] + (idx > 0 ? f[idx - 1] : 0) + 2;
// f[i] = (idx > 0 ? f[idx - 1] : 0) + 2
}
}
if(ans < f[i]) ans = f[i];
} return ans;
}
}
通配符匹配
空字符串情况
空字符串用 \(s[0]\) 表示
如果模板 \(p\) 和串 \(s\) 都为空,\(return\,\,f[0][0]\) 一定为 \(true\)
如果串为空,但模板不为空,模板只有 * 能匹配
遍历:
for j in range(p.length):
if p[j - 1] is "*":
f[0][j] = True
else break
思路分析
- 问题的子结构:
- 原问题:\(s[0,\,n)\) 和 \(p[0,\,m)\) 是否匹配
- 子问题:\(s\) 的子串 \([0,\,i)\) 和 \(p\) 的子串 \(p[0,\,j)\) 是否匹配
- 定义域:\((n,m)\)
- 进入下一阶段时的情况:
- \(s[i] = p[j]\,\,->f[i][j] = f[i-1][j-1]\)
- \(s[i] \neq p[j]\)
- \(p[i] = "?"\,\,->f[i][j] = f[i-1][j-1]\)
符合 ? 或相等 条件 - 继承上个状态的积累 - \(p[i] = "*"\,\,->f[i][j] = f[i][j-1]\,\,OR\,\,f[i-1][j]\)
符合 * 条件 - 继承 最近最多 状态的积累,只继承 - \(else\,\,False;\)
不符合条件,False,清除积累
- \(p[i] = "?"\,\,->f[i][j] = f[i-1][j-1]\)
- 状态转移方程:
\[f(i,j)= \begin{cases} f(i-1,\,j-1) ,\,\,\,(s[i]=p[j])\,\,\bigvee\,\,\,(s[i]\neq p[j]\bigwedge p[j]="?")\\ f(i,\,\,j-1)\,\bigvee\,f(i-1,\,\,j),\,s[i]\neq p[j]\bigwedge p[j]="*"\\ False,\,\,\,\,else \end{cases} \]
代码实现(12ms击败88%)
class Solution {
public boolean isMatch(String s, String p) {
char [] sc = s.toCharArray();
char [] pc = p.toCharArray();
boolean [][] f = new boolean[sc.length + 1][pc.length + 1];
f[0][0] = true;
for(int j = 1; j <= pc.length; j++){
if(pc[j - 1] == '*'){
f[0][j] = true;
} else break;
}
for(int i = 1; i <= sc.length; i++){
for(int j = 1; j <= pc.length; j++){
if(sc[i - 1] == pc[j - 1])
f[i][j] = f[i - 1][j - 1];
else {
if(pc[j - 1] == '?')
f[i][j] = f[i - 1][j - 1];
else if(pc[j - 1] == '*')
f[i][j] = f[i][j - 1] || f[i - 1][j];
}
}
} return f[sc.length][pc.length];
}
}