动态规划之空间优化与总结回顾
Posted 五分钟学算法
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划之空间优化与总结回顾相关的知识,希望对你有一定的参考价值。
点击蓝色“五分钟学算法”关注我哟
加个“星标”,天天中午 12:15,一起学算法
作者 | P.yh
来源 | 五分钟学算法
导言
之前我们就动态规划问题进行了分类与讨论,这篇文章会讲解一些常见的动态规划题目分析技巧和套路,还有一个动态规划状态数组的空间优化技巧,并基于之前的文章进行总结。
切题套路
首先我们需要明确一个问题,那就是如何确定一个问题能够使用动态规划来解?
之前的动态规划文章中,我们清楚知道这些例题就是要用动态规划求解,因此不会去考虑其它的算法或思想。
那怎么通过一道题的题目描述确定这道题适不适合用动态规划解呢?
这里我不想讲特别理论的东西,比如最优子结构,重叠子问题,不讲并不代表这些概念不重要,而是说这些东西有时候并不是很容易从题目描述中简单推得,特别是在面试这种紧张的状态下。如果你仔细观察我们之前解过的所有动态规划相关的题目,题目无外乎下面几种问法:
最值问题(比如,最长公共子序列、最小编辑距离、背包能装的最大价值…)
所有方案的个数(比如,两点之间路径个数、爬楼梯问题…)
是否类问题(比如,两个字符串是否按一定方式匹配、背包是否能被填满…)
上面给出的几种问法都是动态规划题目很常见的问法,如果题目中含有这类信息,你就可以尝试着往动态规划的思路去思考,试着去拆解问题、定义状态等等,当然没有绝对,并不是说所有这种问法的题目都可以用动态规划解,只是大概率是这样,解题的时候多一个思考方向总不是坏事。
当然,还有些题目基本上不大可能使用动态规划求解:
找出所有的方案(比如,N 皇后问题、求解一个集合的所有子集、排列组合类问题…)
排序相关的问题(比如,滑动窗口类问题、双指针类问题…)
极值类问题(比如,求数组当中的峰值…)
这里我解释一下,上面的第一点 “找出所有的方案” 意思是让你列出所有的情况,而不是仅仅输出方案的个数,很多时候,这类题目的方案的总数是随着输入变量呈指数型增长的,因为要列出所有方案,因此再怎么优化,你的时间复杂度还是指数级别的,动态规划在这边起不到作用,这种题目往往会优先考虑使用 暴力的深度优先搜索 来求解。
第二点 “排序相关的问题”,有时这类问题的描述和动态规划题目的描述很像,比如最大滑动窗口,但这类题目区别于动态规划的地方在于,其答案往往并不是一个固定的数值,比如滑动窗口类问题最后让你输出的是一个数组,Two Sum 相关的问题最后也是让你找到匹配的元素,对于这些问题都有固定的套路,在这我就不过多赘述了。最后一点 极值类问题,这类问题出现的频率不是很高,但是你还是需要将其和最值类问题区分开来对待。
在这里,我列出了我们之前讲解的动态规划的几种题型:
上面括号中是针对对应题型的思考方向和分析策略,你也可以回顾下这些内容,分析一下这些动态规划题型的相同和不同之处,加深一下理解。
空间优化 - 滚动数组
有一个比较通用的空间优化技巧没有在之前的文章中提到,很多的动态规划题目都可以套用这个技巧,我们就拿之前的 最长公共子序列 这道题目来举例说明,当时我们最终实现的代码是这样的:
public int longestCommonSubsequence(String text1, String text2) {
int length1 = text1.length();
int length2 = text2.length();
int[][] dp = new int[length1 + 1][length2 + 1];
char[] textArr1 = text1.toCharArray();
char[] textArr2 = text2.toCharArray();
for (int i = 1; i <= length1; ++i) {
for (int j = 1; j <= length2; ++j) {
if (textArr1[i - 1] == textArr2[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[length1][length2];
}
你仔细观察代码,会发现当前考虑的状态 dp[i][j]
仅仅依赖于 dp[i - 1][j]
和 dp[i][j - 1]
,如果画出表格,也就是当前行的格子只会和当前行以及前一行的格子有关,因此,保留两行数据就能够满足状态迭代更新的要求,我们可以得到下面的代码:
public int longestCommonSubsequence(String text1, String text2) {
int length1 = text1.length();
int length2 = text2.length();
int[][] dp = new int[2][length2 + 1];
char[] textArr1 = text1.toCharArray();
char[] textArr2 = text2.toCharArray();
for (int i = 1; i <= length1; ++i) {
for (int j = 1; j <= length2; ++j) {
if (textArr1[i - 1] == textArr2[j - 1]) {
dp[i%2][j] = dp[(i - 1)%2][j - 1] + 1;
} else {
dp[i%2][j] = Math.max(dp[(i - 1)%2][j], dp[i%2][j - 1]);
}
}
}
return dp[length1%2][length2];
}
这里我们成功将空间的维度降低了一维,当然如果你觉得取模的操作让代码变得不整洁,你也可以参考下面的代码:
public int longestCommonSubsequence(String text1, String text2) {
int length1 = text1.length();
int length2 = text2.length();
int[][] dp = new int[2][length2 + 1];
char[] textArr1 = text1.toCharArray();
char[] textArr2 = text2.toCharArray();
int cur = 0, prev = 1;
for (int i = 1; i <= length1; ++i) {
prev = cur; cur = 1 - cur;
for (int j = 1; j <= length2; ++j) {
if (textArr1[i - 1] == textArr2[j - 1]) {
dp[cur][j] = dp[prev][j - 1] + 1;
} else {
dp[cur][j] = Math.max(dp[prev][j], dp[cur][j - 1]);
}
}
}
return dp[cur][length2];
}
其实滚动数组的思想不难理解,就是只保存需要用到的子问题的答案(状态),覆盖那些不需要用到的子问题的答案,状态在同一块空间中不断翻滚迭代向前。
当然,有些动态规划的实现方式就不太容易使用这类优化,比如 记忆化搜索,还有些动态规划题型,比如 区间类动态规划,状态的更新不是逐行逐列的,使用滚动数组来优化也不是特别容易,因此使用滚动数组优化的时候还是需要结合实际情况考虑。
滚动数组一般来说都可以将状态数组的空间降低一维,比如三维变二维、二维变一维、一维变常数,当然有些具体题型的空间优化也可以做到这个,比如背包类型的动态规划问题中,我们通过改变遍历的顺序,直接就可以做到空间降维,但其实这是这类动态规划问题特有的优化,不属于滚动数组的范畴。
总结
动态规划系列内容算是结束了,虽然有关动态规划的知识点还有很多,但是我相信如果深刻掌握并理解了之前我们讲的内容,基本上 leetcode 上面 90% 以上的动态规划相关问题都可以很好解决。
当然了,要想达到熟能生巧的程度,还是需要多加练习,多思考,多对比,多总结,不然的话,学到的东西很快就会忘记。
最后的最后,希望动态规划不再是你面试中的拦路虎,看到它,也希望你能多一份亲切和自信。
戳一下下方的小程序,24 小时一起学算法
以上是关于动态规划之空间优化与总结回顾的主要内容,如果未能解决你的问题,请参考以下文章
LeetCodeLeetCode之打家劫舍暴力递归动态规划动态规划之优化空间的具体分析与实现
LeetCodeLeetCode之跳跃游戏——动态规划+贪心算法
LeetCodeLeetCode之删除并获得点数——动态规划排序+动态规划
详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++