算法刷题:DP专题(9.16,持续更)
作者:@罗一
本文为作者原创,转载请注明出处:https://www.cnblogs.com/luoyicode/p/17707257.html
算法刷题系列上期:
力扣链接:
递归中的状态转移方程:动态规划题目:
目录
动态规划基础
递归 / 多阶段最优问题
多个阶段操作后的最优问题,如果可以转化为根据新阶段的操作结果 累加到对应旧阶段操作 累计 的结果,这是典型的递归思路,也是状态转移
子问题和状态
状态就是子问题的解
具体的说‘阶段’概念
数据容器 下一个迭代位置
- 打家劫舍 的房子,就是数组;
- LCS 和 LRA 的 s1、s2 就是数组 / 串;
- 最长有效括号 的载体也是数组 / 串;
- 通配符匹配 的下一个阶段也是数组 / 串的 最新索引位置
等等...
状态和阶段的区别
- 状态是从一组元素中计算的结果;
- 阶段是从一个元素中计算;
状态转移方程
i - 阶段下 - 决策的定义域、f - 决策 / 问题处理 函数、f(i) - 状态
意义
- 递归树的剪枝
- 递归子问题/层层决策 的 全局最优解,避免贪心的鼠目寸光
- 空间换时间,提高时间复杂度
最后说说解题思路
- 找出 单阶段的 操作 和 条件判断
- 找出 原问题 / 子问题 / 状态
- 确认 单个阶段的操作(1个) 和 子问题 / 旧状态(1组) 的关系
- 列出 状态转移方程
递归中的状态转移方程
最长同值路径(medium)
列出状态转移方程
当前状态作为原问题 需解决的 / 递 的子问题、向上层贡献 / 归:
当前状态作为 原问题本身,看看能不能全局最优:
代码实现(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)
列出状态转移方程
作为原问题的子树、子问题的解(返回给顶层):
作为原问题,看当前局部 是否是 全局最大值:
代码实现 (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)
思路分析
- 当前阶段?到 这个位置,从
或 的台阶到 位置 - 一组阶段?如何从
位置到 / - 状态转移方程:略
代码实现:
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 节点的最短路径
- 定义域:
- 状态转移方程:
代码实现(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 项进行匹配,找到最长重复子串
- 定义域:
表示 串的尾端位置, 表示 串的尾端位置 - 状态值:
表示 a[i] 和 b[j] 匹配时,最后位置( )已经匹配到的子串长度 - 状态前进:
- 状态转移方程:
代码实现(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 前缀已经匹配到的最长子序列
- 定义域:
- 进入下一阶段时的情况:
- 如果
,那么当前阶段要在上一阶段状态的基础上
符合条件,继承上个状态的积累,并加上 新的一个单位的积累 - 如果
,那么要找到 不同时出现的情况的最大边界;
不符合条件,继承 最近最多 状态的积累,只继承
- 如果
- 状态转移方程:
代码实现(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)
所求问题无后效性,就一定作为状态吗?
所求问题可能不好维护;
- 比如求一段区间内符合条件的最长子串,如果这段区间里有很多段子串,随着区间的扩大,维护多个子串的代价很大
- 所以不能因为所求问题是无后效性的,就将其作为状态维护起来,可能需要简化问题,让状态尽可能单一
思路分析
- 问题的子结构:
- 所求问题:
区间上的最长子串 - 简化后的原问题:
区间上,以 位置为尾端的子串的长度- 子问题:
- 定义域:
- 状态转移方程:
代码实现(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;
}
}
通配符匹配
空字符串情况
空字符串用
如果模板
如果串为空,但模板不为空,模板只有 * 能匹配
遍历:
for j in range(p.length):
if p[j - 1] is "*":
f[0][j] = True
else break
思路分析
- 问题的子结构:
- 原问题:
和 是否匹配 - 子问题:
的子串 和 的子串 是否匹配
- 原问题:
- 定义域:
- 进入下一阶段时的情况:
符合 ? 或相等 条件 - 继承上个状态的积累
符合 * 条件 - 继承 最近最多 状态的积累,只继承
不符合条件,False,清除积累
- 状态转移方程:
代码实现(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];
}
}
背包DP题目
分割等和子集
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现