问题引入
哪种情况下可以可以对dp表进行滚动数组优化?
直观的讲,在某一维度的跨度仅仅是1的时候(出现 i - 1 或者 j - 1 ),就可以直接删掉一维,即进行滚动数组优化。
滚动数组优化的代码怎么写?
由于滚动数组会让原先清晰的动态规划逻辑变的莫名其妙,因而最后不要想着一步到位、直接写低维dp。而是直接在原先代码的基础上删掉一维即可。
可是,这种屡试不爽的思路每用10次翻车1次,究竟是为什么呢?
看来,上面这种“一维跨度”的思路并没有触及到滚动数组的本质,是一个错误的认知。下面,将用一个极为典型的例题,带你彻底弄懂滚动数组,与面试官侃侃而谈。
典型例题
【题目】最长公共子序列
【描述】给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。其中,子序列可以是原序列中不连续的元素。如果不存在公共子序列,返回 0 。
【输入】text1 = “abcde”, text2 = “ace”
【输出】3
【解释】最长公共子序列是 “ace” ,它的长度为 3 。
题目本身非常简单。这里直接给出代码。
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
char[] arr1 = text1.toCharArray();
char[] arr2 = text2.toCharArray();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (arr1[i - 1] == arr2[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[m][n];
}
}
滚动数组(重点!)
不难发现,状态转移方程的跨度都是1,是不是就说明我们可以无脑滚动数组了呢?
不妨在草稿纸上试一试:
我们突然意识到滚动数组的本质:
在不影响其他状态转移的前提下,进行状态值的覆盖,从而复用了数组空间。
而滚动数组安全性的前提是:
被覆盖的状态值是不会再被用到的;如果之后还会被用到,你却把它覆盖掉了,即“影响了其他状态转移”。
强行滚动(重点!)
我们已经知道,本题的dp表在滚动时,会出现有效状态值的覆盖。所以我们就不能进行滚动数组优化了吗?
的确,不能再使用无脑的滚动数组优化。
但是,我们可以强行让它滚动起来。
如何实现?在有效状态值被覆盖之前,将它记忆下来。
下面的写法借助了一个临时的tmp,注意理解:
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
char[] arr1 = text1.toCharArray();
char[] arr2 = text2.toCharArray();
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++) {
int pre = 0;
for (int j = 1; j <= n; j++) {
int tmp = dp[j];
if (arr1[i - 1] == arr2[j - 1]) {
dp[j] = pre + 1;
} else {
dp[j] = Math.max(dp[j], dp[j - 1]);
}
pre = tmp;
}
}
return dp[n];
}
}
写在后面
虽然dp[][]的空间被压缩为dp[],看但时间复杂度丝毫不变————滚动数组只不过是一种"原地操作"罢了。
E N D END END