问题
对于以下一段代码:代码来源:labuladong
int longestPalindromeSubseq(string s) {
int n = s.size();
// dp 数组全部初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)
dp[i][i] = 1;
// 反着遍历保证正确的状态转移
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 状态转移方程
if (s[i] == s[j])
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
// 整个 s 的最长回文子串长度
return dp[0][n - 1];
}
经过状态压缩可以变为:
for (int i = n - 2; i >= 0; i--) {
// 存储 dp[i+1][j-1] 的变量
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
if (s[i] == s[j])
// dp[i][j] = dp[i+1][j-1] + 2;
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
// 到下一轮循环,pre 就是 dp[i+1][j-1] 了
pre = temp;
}
}
分析
下面将围绕上述代码块进行分析。
为什么可以这么做?我想探讨一下其背后的思想。
个人认为,最主要的原因在于:
抓住循环运行的顺序,将外层循环变量i
的职责,交给时间差来完成
如何理解这句话?
假设动态规划的过程是
上述代码的运行过程为:内层j
从小到大,外层i
从大到小的顺序运行。
那么,如果我们抛去外层循环i
,把i
当成一个时刻。
此刻运行到了时刻i
,根据循环的顺序,上一个时刻就是i+1
那么,dp[i][j] = function(dp[i+1][j])
就等同于 dp[j] = function(dp[j])
而对于 j - 1
, j - 2
等等,由于我们此刻的操作并不会进行覆盖,那么它的值是此刻还是以前都不影响。
所以
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
等同于
dp[j] = max(dp[j], dp[j - 1]);
便可以理解了。
那
dp[i][j] = dp[i + 1][j - 1] + 2;
等同于
dp[j] = pre + 2;
要如何理解呢?
原因就在于,当进行到此刻i
时(铭记,i
我们已经当成了时间概念),
dp[j-1]
已经先于dp[j]
被更新,这就意味着,dp[j-1]
此刻即为dp[i][j-1]
,而上一时刻的值:dp[i+1][j-1]
已经被无情地覆盖了。但我们在更新dp[i][j]
时却要用到它,那么该怎么办呢?
这就要求我们,每次更新dp[j]
时,将它更新之前的值记录下来,留给下一个j
使用。
比如,时刻i = 3
, 我们准备更新j = 4
,此时,j = 4
位置的值实际为 dp[4][4]
,(上一时刻i + 1
)
我们保存下来到pre
之后,更新j = 4
,此时该位置的值为 dp[3][4]
当我们到达 i = 3
, j = 5
时,就能够拿到pre的值 dp [4][4]
,即dp [i + 1][j-1]
了
按照上述说法,每次j循环一开始,更新前,就要记录下来该位置的旧值。所以temp变量在内循环的第一句就出现,就是这个原因。
for (int i = n - 2; i >= 0; i--) {
// 存储 dp[i+1][j-1] 的变量
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
...