学习周报W04
前言
从 递归 到 记忆化搜索
从 记忆化搜索 到 动态规划
由顶至底,由底至顶。
无论怎么说,都是对所有状态的穷举。
而如何找出,定义这些状态,是我们所要做的,
这就是 动态规划。
一维DP
前置知识:
需要熟悉递归
关于取模,需要熟悉 同余原理
动态规划:用空间代替重复计算
知道怎么算的算法 vs 知道怎么试的算法
有些递归在展开计算时,总是重复调用同一个子问题的解,这种重复调用的递归变成动态规划很有收益
如果每次展开都是不同的解,或者重复调用的现象很少,那么没有改动态规划的必要
任何动态规划问题都一定对应着一个有重复调用行为的递归
所以任何动态规划的题目都一定可以从递归入手,逐渐实现动态规划的方法
尝试策略 就是 转移方程,完全一回事!
推荐从尝试入手,因为代码好写,并且一旦发现尝试错误,重新想别的递归代价轻!
当熟悉了从递归到动态规划的转化过程
那么就可以纯粹用动态规划的视角来分析问题了
题目5到题目8,都是纯粹用动态规划的视角来分析、优化的
如果不熟悉这个过程,直接一上来就硬去理解状态转移方程
那么往往会步履维艰、邯郸学步、东施效颦
很多极为优异的想法、设计和优化 来自 努力 or 天赋
建议脚踏实地,真正做好从递归到动态规划的练习
例题分析:
题目1
斐波那契数
测试链接 : https://leetcode.cn/problems/fibonacci-number/
1 | 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 |
注意:
斐波那契数问题最经典,常见方法时间复杂度O(n)
但是最优解来自矩阵快速幂,时间复杂度可以做到O(log n)
辅助理解:从 朴素递归 到 记忆化搜索 再到 动态规划 再到 动态规划的优化 - 滚动数组。
题目2
最低票价
测试链接 : https://leetcode.cn/problems/minimum-cost-for-tickets/
1 | 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行 |
题目3
解码方法
测试链接 : https://leetcode.cn/problems/decode-ways/
1 | 一条包含字母 A-Z 的消息通过以下映射进行了 编码 : |
题目4
解码方法 II
测试链接 : https://leetcode.cn/problems/decode-ways-ii/
1 | 一条包含字母 A-Z 的消息通过以下的方式进行了 编码 : |
前置题目:91. 解码方法
题目5
丑数 II
测试链接 : https://leetcode.cn/problems/ugly-number-ii/
1 | 丑数 就是只包含质因数 2、3 或 5 的正整数 |
利用了三指针动态规划解题。
三指针法不是严格意义上的线性筛,但具有类似的线性时间特性。
它是一种动态规划(DP)策略,而非筛法(Sieve)。
题目6
最长有效括号
测试链接 : https://leetcode.cn/problems/longest-valid-parentheses/
1 | 给你一个只包含 '(' 和 ')' 的字符串 |
DP方法和栈方法。
栈方法的思想是用栈保留 失配位置。
题目7
环绕字符串中唯一的子字符串
测试链接 : https://leetcode.cn/problems/unique-substrings-in-wraparound-string/
1 | 定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串 |
题目8
不同的子序列 II
测试链接 : https://leetcode.cn/problems/distinct-subsequences-ii/
1 | 给定一个字符串 s,计算 s 的 不同非空子序列 的个数 |
前置题目:115. 不同的子序列
总结:
动态规划的大致过程:
想出设计优良的递归尝试(方法、经验、固定套路很多),有关尝试展开顺序的说明
- 记忆化搜索(从顶到底的动态规划) ,如果每个状态的计算枚举代价很低,往往到这里就可以了
- 严格位置依赖的动态规划(从底到顶的动态规划) ,更多是为了下面说的 进一步优化枚举做的准备
- 进一步优化空间(空间压缩),一维、二维、多维动态规划都存在这种优化
- 进一步优化枚举也就是优化时间
解决一个问题,可能有很多尝试方法
众多的尝试方法中,可能若干的尝试方法有重复调用的情况,可以转化成动态规划
若干个可以转化成动态规划的方法中,又可能有优劣之分
判定哪个是最优的动态规划方法,依据来自题目具体参数的数据量
最优的动态规划方法实现后,后续又有一整套的优化技巧
二维DP
前置知识:
- 递归
- 从递归入手一维动态规划
从递归入手二维动态规划
尝试函数有1个可变参数可以完全决定返回值,进而可以改出1维动态规划表的实现
同理
尝试函数有2个可变参数可以完全决定返回值,那么就可以改出2维动态规划的实现
一维、二维、三维甚至多维动态规划问题,大体过程都是:
- 写出尝试递归
- 记忆化搜索(从顶到底的动态规划)
- 严格位置依赖的动态规划(从底到顶的动态规划)
- 空间、时间的更多优化
动态规划表的大小:每个可变参数的可能性数量相乘
动态规划方法的时间复杂度:动态规划表的大小 * 每个格子的枚举代价
二维动态规划依然需要去整理 动态规划表的格子之间的依赖关系
找寻依赖关系,往往 通过画图来建立空间感,使其更显而易见
然后依然是 从简单格子填写到复杂格子 的过程,即严格位置依赖的动态规划(从底到顶)
二维动态规划的压缩空间技巧原理不难,会了之后千篇一律
但是不同题目依赖关系不一样,需要 很细心的画图来整理具体题目的依赖关系
最后进行空间压缩的实现
什么递归能改成动态规划?
能改成动态规划的递归,统一特征:
决定返回值的可变参数类型往往都比较简单,一般不会比int类型更复杂。为什么?
从这个角度,可以解释 带路径的递归(可变参数类型复杂),不适合或者说没有必要改成动态规划
题目2就是说明这一点的
一定要 写出可变参数类型简单(不比int类型更复杂),并且 可以完全决定返回值的递归,
保证做到 这些可变参数可以完全代表之前决策过程对后续过程的影响!再去改动态规划!
不管几维动态规划
经常从递归的定义出发,避免后续进行很多边界讨论
这需要一定的经验来预知
例题分析:
题目1
最小路径和
测试链接 : https://leetcode.cn/problems/minimum-path-sum/
1 | 给定一个包含非负整数的 m x n 网格 grid |
题目2
单词搜索(无法改成动态规划)
测试链接 : https://leetcode.cn/problems/word-search/
1 | 给定一个 m x n 二维字符网格 board 和一个字符串单词 word |
上位题目:212. 单词搜索 II <- 利用 DFS + 前缀树剪枝 解题
题目3
最长公共子序列
测试链接 : https://leetcode.cn/problems/longest-common-subsequence/
1 | 给定两个字符串text1和text2 |
题目4
最长回文子序列
测试链接 : https://leetcode.cn/problems/longest-palindromic-subsequence/
1 | 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度 |
- 用原始串和逆序串转化为 LCS问题 即 题目3
- 区间DP思路
题目5
节点数为n高度不大于m的二叉树个数
测试链接 : https://www.nowcoder.com/practice/aaefe5896cce4204b276e213e725f3ea
1 | 现在有n个节点,计算出有多少个不同结构的二叉树 |
题目6
矩阵中的最长递增路径
测试链接 : https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/
1 | 给定一个 m x n 整数矩阵 matrix ,找出其中 最长递增路径 的长度 |
做到记忆化搜索就行,依赖关系混乱还不如不DP
二维DP - 拓展 - 直接从动态规划的定义入手
前置知识:
- 二维DP - 从递归入手二维动态规划
例题分析:
题目1
不同的子序列
测试链接 : https://leetcode.cn/problems/distinct-subsequences/
1 | 给你两个字符串s和t |
题目2
编辑距离
测试链接 : https://leetcode.cn/problems/edit-distance/
1 | 给你两个单词 word1 和 word2 |
实际上就是 LCS问题
题目3
交错字符串
测试链接 : https://leetcode.cn/problems/interleaving-string/
1 | 给定三个字符串 s1、s2、s3 |
看起来像三维,实际上第三个参数是由第一和第二个决定的。
题目4
有效涂色问题
1 | 给定n、m两个参数 |
题目5
删除至少几个字符可以变成另一个字符串的子串
1 | 给定两个字符串s1和s2 |
三维DP
前置知识:
- 从递归入手二维动态规划
从递归入手三维动态规划
尝试函数有1个可变参数可以完全决定返回值,进而可以改出1维动态规划表的实现
同理
尝试函数有2个可变参数可以完全决定返回值,那么就可以改出2维动态规划的实现
同理
尝试函数有3个可变参数可以完全决定返回值,那么就可以改出3维动态规划的实现
大体过程都是:
- 写出尝试递归
- 记忆化搜索(从顶到底的动态规划)
- 严格位置依赖的动态规划(从底到顶的动态规划)
- 空间、时间的更多优化
例题分析:
题目1
一和零(多维费用背包)
测试链接 : https://leetcode.cn/problems/ones-and-zeroes/
1 | 给你一个二进制字符串数组 strs 和两个整数 m 和 n |
题目2
盈利计划(多维费用背包)
测试链接 : https://leetcode.cn/problems/profitable-schemes/
1 | 集团里有 n 名员工,他们可以完成各种各样的工作创造利润 |
题目3
骑士在棋盘上的概率
测试链接 : https://leetcode.cn/problems/knight-probability-in-chessboard/
1 | n * n的国际象棋棋盘上,一个骑士从单元格(row, col)开始,并尝试进行 k 次移动 |
题目4
矩阵中和能被 K 整除的路径
测试链接 : https://leetcode.cn/problems/paths-in-matrix-whose-sum-is-divisible-by-k/
1 | 给一个下标从0开始的 n * m 整数矩阵 grid 和一个整数 k |
题目5
扰乱字符串
测试链接 : https://leetcode.cn/problems/scramble-string/
1 | 使用下面描述的算法可以扰乱字符串 s 得到字符串 t : |
子数组最大累加和 - Kadane’s算法
前置知识:
- 二分答案法
- 从递归入手一维动态规划
子数组最大累加和问题是一个非常经典的问题,也比较简单
为什么叫 Kadane’s算法 ?
Kadane 当时是美国卡内基梅隆大学(Carnegie Mellon University)的统计学家。他在一次内部讨论中,提出了解决最大子数组和问题的线性时间算法,这个问题最初是为了解决股票收益最大化场景(即找出某段时间内的最佳买卖时机)。
这套算法后来被广泛传播并引用,逐渐被称为:
Kadane’s Algorithm
(Kadane 的算法)
例题分析:
题目1
测试链接 : https://leetcode.cn/problems/maximum-subarray/
1 | 子数组最大累加和 |
题目2
不能选相邻元素的最大累加和问题
测试链接 : https://leetcode.cn/problems/house-robber/
1 | 给定一个数组,可以随意选择数字,但是不能选择相邻的数字 |
题目3
环形数组的子数组最大累加和
测试链接 : https://leetcode.cn/problems/maximum-sum-circular-subarray/
1 | 给定一个数组nums,长度为n |
两种情况
- 最大累加和子数组没有被分割:转化为 问题一
- 最大累加和子数组被分割:实际上就是 子数组总和 - 子数组最小累加和
另外 要警惕 子数组最小累加和 == 子数组总和 的特例
题目4
环形数组中不能选相邻元素的最大累加和
测试链接 : https://leetcode.cn/problems/house-robber-ii/
1 | 给定一个数组nums,长度为n |
两种情况
- 要 0 位置上的数,问题转化为:在(1, size - 1)范围上求 问题2
- 要 size - 1 位置上的数,问题转化为:在(0, size - 2)范围上求 问题2
题目5
打家劫舍 IV
测试链接 : https://leetcode.cn/problems/house-robber-iv/
1 | 沿街有一排连续的房屋。每间房屋内都藏有一定的现金 |
二分答案 + 问题2
check函数的设计就是 问题2
因为题目只要求能偷的房间数,
所以在此基础上还可以继续贪心优化减小常数时间,
贪心思路:能偷就偷,不能偷就移动到下一个能偷的点。
题目6
子矩阵最大累加和问题
测试链接 : https://leetcode.cn/problems/max-submatrix-lcci/
1 | 给定一个二维数组grid,找到其中子矩阵的最大累加和 |
压缩数组转化为 问题1
子数组最大累加和(下)
前置知识:
- 对数器
- 构建前缀信息的技巧
- 单调队列,一定要掌握,本节课题目6需要,后续讲“多重背包的单调队列优化”也需要
- 子数组最大累加和问题与扩展-上
例题分析:
题目1
乘积最大子数组
测试链接 : https://leetcode.cn/problems/maximum-product-subarray/
1 | 给你一个double类型数组 nums |
与 最大子数组累加和 问题的差异点在于:考虑负负得正情况,
即有可能最优解来自于最小的累乘积与当前负数的积。
题目2
子序列累加和必须被7整除的最大累加和
1 | 给定一个非负数组nums, |
dp[i][j]
定义:nums 前 i 个数中,形成的子序列累加和 % 7 == j
最终答案返回dp[n][0]
题目3
魔法卷轴
1 | 给定一个数组nums,其中可能有正、负、0 |
三种情况:
- 不用:累加。
- 用一次:维护一个 一定用了一次卷轴 的 前缀和数组 即 在0 ~ i范围上 前缀和最好 的 前缀和数组。
- 用两次:同上,不过是构建 特殊后缀和数组,然后枚举划分点。
题目4
三个无重叠子数组的最大和
测试链接 : https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/
1 | 给你一个整数数组 nums 和一个整数 k |
建立辅助信息:sums[i]
: 以 i 开头 长度为k的 的 区间累计和。prefix[i]
: 在 range(0, i) 上 长度为k的子数组 中 最大sum 的子数组 的 开头在哪。suffix[i]
: 在 range(i, n - 1) 上 长度为k的子数组 中 最大sum 的子数组 的 开头在哪。
三个划分:range1(0, i - 1) <-> range2(i, j) <-> range3(j + 1, n - 1)
range1可以从prefix获取,range3可以从suffix获取,只额外需要枚举range2即可。
题目5
可以翻转1次的情况下子数组最大累加和
1 | 给定一个数组nums, |
分析要点:剖析 翻转区间 与 答案区间 的关系。
枚举思路:翻转区间)+(以 i 开头的答案区间
这样我们就可以关注 以 i 开头往右延伸的的最大和区间最大值
和 以 i - 1 开头往左延伸的最大和区间最大值
题目6
删掉1个数字后长度为k的子数组最大累加和
1 | 给定一个数组nums,求必须删除一个数字后的新数组中 |
问题转化为,在原数组中长度为 k + 1 的子数组中最大累加和减去 子数组中最小值。
最长递增子序列
前置知识:
- 二分搜索
- 一维动态规划
【算法讲解072【必备】最长递增子序列问题与扩展】
补充:该问题也可以线段树求解
例题分析
题目1
最长递增子序列
测试链接 : https://leetcode.cn/problems/longest-increasing-subsequence/
1 | 给定一个整数数组nums |
- O(n^2)解
dp[i]
: 以 i 结尾的最长严格递增子序列长度 - O(nlb(n))解(最优解)
dp[i]
: 以 i 结尾的最长严格递增子序列长度
ends[i]
: 目前所有长度为 i+1 的递增子序列的最小结尾
实际上就是维护了一个具有单调性的 ends数组 方便二分搜索
题目2
俄罗斯套娃信封问题
测试链接 : https://leetcode.cn/problems/russian-doll-envelopes/
1 | 给你一个二维整数数组envelopes |
定义一个排序规则,按照宽度从小到大排列且同样宽度的就按照高度从大到小排列
然后排序后单独关注高度求 最大严格递增子序列问题即可
这样就保证了单独关注高度时,某个高度的 信封 左侧的信封宽度一定小于等于当前信封
这样做可以分离宽度对高度的干扰
题目3
使数组K递增的最少操作次数
测试链接 : https://leetcode.cn/problems/minimum-operations-to-make-the-array-k-increasing/
1 | 给你一个下标从0开始包含n个正整数的数组arr,和一个正整数k |
实际上就是将 原数组 分为 k 组,这 k组 数据就互不干扰了,
然后对 k组 数据操作将其变为单调递增序列,实际上就是对其求最长不下降子序列长度。
题目4
最长数对链
测试链接 : https://leetcode.cn/problems/maximum-length-of-pair-chain/
1 | 给你一个由n个数对组成的数对数组pairs |
实际上也是最长递增子序列问题
不过维护 ends数组 的过程中,需要注意,在长度相同的情况下,只维护尽可能小的右部分值。
查询与更新分离的思想
贪心解法:以右部值排序,用一个 pre 变量跟踪前一个右部值,遍历数对,能连接的就连接。
题目5
有一次修改机会的最长不下降子序列
测试链接 : https://www.luogu.com.cn/problem/P8776
1 | 给定一个长度为n的数组arr,和一个整数k |
看视频去
cur
…i)(…k…)(j…
三个range,三个贡献。
同时,查询与更新分离的思想。
有点像前一题,数对。
数对的形式是(l, r)
这一题的形式是(l, range(k), r)
预告
最长递增子序列的数量问题
给定一个未排序的整数数组nums,返回最长递增子序列的个数
测试链接 : https://leetcode.cn/problems/number-of-longest-increasing-subsequence/
这个问题的最优解能做到O(n * logn)
会放在【扩展】课程阶段,详解树状数组(index tree)的时候来讲解
用这个高级数据结构来求解这个题会很方便
这里为什么要提呢?主要是想说:巧妙构思 vs 成熟体系
广阔视野带来的高维思考,这就是启发