子序列解题模板:最长回文子序列
一. 前言
一般来说,这类问题都是让你求一个最长子序列。一旦涉及到子序列和最值,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。
原因很简单,你想想一个字符串,它的子序列有多少种可能?起码是指数级的吧,这种情况下,不用动态规划技巧,还想怎么着呢?
既然要用动态规划,那就要定义 dp 数组,找状态转移关系。我们说的两种思路模板,就是 dp 数组的定义思路。不同的问题可能需要不同的 dp 数组定义来解决。
二. 两种思路
- 第一种思路模板是一个一维的dp数组:
int n = array.length;
int []dp = new int[n];
for(int i =1;i<n;i++){
for(int j=0;j<i;j++){
dp[i] = 最值(dp[i],dp[j]+...)
}
}
举个最长递增子序列的例子,这个思路中dp数组的定义是:
在子数组array[0…i]中,以array[i]结尾的目标子序列(最长递增子序列)的长度是dp[i]。
- 第二种思路模板是一个二维的dp数组:
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 1; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组arr1[0…i]和子数组arr2[0…j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]。
只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
在子数组array[i…j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]。
三.最长回文子序列
首先回顾一下,对单个字符串的最长回文子序列问题,对dp数组的定义是: 在子串s[i…j]中,最长回文子序列的长度为dp[i][j]。
这样定义二维的dp数组的原因是,找状态转移需要归纳的思维,说白了就是如何从已知结果推出未知部分。
具体来说,如果知道了子问题dp[i+1][j-1]的结果:
对于字符s[i]和s[j] , 如果它俩相等,那么它俩加上s[i+1…j-1]中的最长回文子序列就是s[i…j]的最长回文子序列;如果它俩不相等,说明它俩不可能同时出现在s[i…j]的最长回文子序列中,那么把它俩分别加入s[i+1…j-1]中,看看哪个子串产生的回文子序列更长即可。
最后我们要求的实际上是dp[0][n-1],也就是整个s的最长回文子序列的长度。
四.代码实现
首先明确base case, 如果只有一个字符,显然最长回文子序列长度是1,也就是dp[i][j] =1(i==j);
对于i>j,根本不存在什么子序列,因此初始化为0。
再看状态转移方程,想求dp[i][j]需要知道dp[i+1][j-1],dp[i+1][j],dp[i][j-1]这三个位置;根据base case,填入数组后应为:
反着遍历:
int longestpal(String s){
int n =s.length();
int [][] dp = new int[n][n];
Arrays.fill(dp,0);
for(int i =0;i<n;i++)
dp[i][i]=1;
//反着遍历保证正确的状态转移
for(int i = n-1;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] = Math.max(dp[i][j-1],dp[i+1][j]);
}
}
return dp[0][n-1];
}
五.总结
主要还是正确定义 dp 数组的含义,遇到子序列问题,首先想到两种动态规划思路,然后根据实际问题看看哪种思路容易找到状态转移关系。
另外,找到状态转移和 base case 之后,一定要观察 DP table,看看怎么遍历才能保证通过已计算出来的结果解决新的问题。