动态规划---最长子串/子序列问题序列1
5. 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解法1:动态规划
- 1、当子串只包含 1 个字符,它一定是回文子串;
- 2、当子串包含 2 个以上字符的时候:如果 s[l, r] 是一个回文串,例如 “abccba”,那么这个回文串两边各往里面收缩一个字符(如果可以的话)的子串 s[l + 1, r - 1] 也一定是回文串,即:如果 dp[l][r] == true 成立,一定有 dp[l + 1][r - 1] = true 成立。
根据这一点,我们可以知道,给出一个子串 s[l, r] ,如果 s[l] != s[r],那么这个子串就一定不是回文串。如果 s[l] == s[r] 成立,就接着判断 s[l + 1] 与 s[r - 1],这很像中心扩散法的逆方法。 - 时间和空间都是o(n^2).
- 强调:
状态定义: dp[i][j] 表示数组子串 s[i, j] 是否为回文子串。
状态转移方程: dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))
class Solution {
public String longestPalindrome(String s) {
if (s.length() <= 1) {
return s;
}
int len = s.length();
int longestStrLength = 1;
String longestStr = s.substring(0,1);
//dp[left][right] 表示从left到right是否是回文子串
boolean[][] dp = new boolean[len][len];
for (int right=0; right<len; right++) {
for (int left=0; left<right; left++) {
//1 如果 dp[l, r] = true 那么 dp[l + 1, r - 1] 也一定为 true,所以反过来递推
//2 剪枝:如果left到right间的数只有一个或者没有,比如left=1> right=2(中间没有数)或者1>3(中间只有一个数),则不用判断,中间的数肯定是回文的
//3 left到right间的数:至少有2个才有判断:dp[left+1][right-1]的必要。
if (s.charAt(left) == s.charAt(right) && (right-left <=2 || dp[left+1][right-1])) {
dp[left][right] = true;
if (right - left + 1 > longestStrLength) {
longestStrLength = right - left + 1;
// 因为substring右开的,所以别忘了+1.
longestStr = s.substring(left, right +1);
}
}
}
}
return longestStr;
}
}
解法2:
- 回文串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断左右字符是否相等即可。由于存在奇数的字符串和偶数的字符串,所以我们需要从一个字符开始扩展,或者从两个字符之间开始扩展,所以总共有 n+n-1 个中心。。这里的n指的是字符串的长度为n,而 n-1是字符之间的*。所以共有2n-1个中心。
- 时间复杂度:O(n²)。
- 空间复杂度:O(1)。
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return "";
}
int start = 0,end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroudCenter(s,i,i); // s长度是奇数
int len2 = expandAroudCenter(s,i,i+1); // s长度是偶数
int len = Math.max(len1,len2);
if (len > end - start) {
start = i - (len-1) / 2;
end = i + len / 2;
}
}
//substring(int beginIndex, int endIndex):左闭右开
//返回一个新字符串,它是此字符串的一个子字符串。该子字符串从指定的 beginIndex 处开始, endIndex:到指定的 endIndex-1处结束。所以下面end+1.
return s.substring(start, end+1);
}
private int expandAroudCenter(String s, int left, int right) {
int L = left, R = right;
while (L >=0 && R <s.length() && s.charAt(L) == s.charAt(R)) {
//注意这里是以中心往两边扩散,所以左--,右++。
L--;
R++;
}
return R-L-1;
}
}
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
解法1 : 动态规划
- 默认每次会选定nums[i] ,这样通过j的枚举来确定在nums[i]之前最长的子序列,最后别忘了+1(把nums[i]带上),这样确定状态转移方程:dp[i] = Math.max(dp[i], dp[j] + 1); 同时状态定义的数组可以确定为是一个一维数组。
- 时间复杂度0(n*n),空间为O(n);
- dp[i] 表示以 nums[i] 结尾的「上升子序列」的长度。注意:这个定义中 nums[i] 必须被选取,且必须是这个子序列的最后一个元素。
class Solution {
public int lengthOfLIS(int[] nums) {
//1 边界值处理
if (nums.length == 0 || nums == null) {
return 0;
}
//2 当nums不为空是,返回的最长子序列长度最少为1
int res = 1;
//3 初始化一个全是1的数组,这样这个最长子序列长度也是1.也是边界处理
int[] dp = new int[nums.length];
for (int i =0; i< nums.length; i++) {
dp[i] = 1;
}
//4 默认每次会选定nums[i] ,这样通过j的枚举来确定在nums[i]之前最长的子序列,最后别忘了+1(把nums[i]带上),可以理解为每次求以nums[i]结尾的最长子序列。
for (int i=1; i < nums.length; i++) {
for (int j=0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);//这里dp[j]每次都为1
}
}
// 在外围大范围内确定最终值
res = Math.max(res,dp[i]);
}
return res;
}
}
解法2:二分解法(推荐)
- 时间复杂度:O(NlogN),遍历nums列表需: O(N),在每个nums[i]二分法需O(logN)。
- 空间复杂度:O(N),tails 列表占用线性大小额外空间。
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
int len = nums.length;
//维护一个tail数组,该数组是递增的,可以进行二分,长度是len+1
int[] tail = new int[len];
tail[0] = nums[0];
//end表示有序数组tail最后一个已经赋值元素的索引
int end = 0;
for (int i=1; i<len; i++) {
//比tail数组最大的元素还大,直接加入tail数组
if (nums[i] > tail[end]) {
end++;
tail[end] = nums[i];
} else {
// 否则在数组tail中二分查找第1个大于等于nums[i]的那个元素,尝试让那个元素更小
int left = 0;
int right = end;
while (left < right) {
// 使用左中位数
int mid = left + ((right - left) >>> 1);
if (tail[mid] < nums[i]) {
//mid更小,则赋值mid+1
left = mid + 1;
} else {
right = mid;
}
}
//一定能找到第 1 个大于等于 nums[i] 的元素
tail[left] = nums[i];
}
}
//end 是有序数组 tail 最后一个元素的索引,所以加1返回
end++;
return end;
}
}