子序列与子数组(子串)
子序列与子数组(子串)
区别
- 子序列
- 元素在原数组的下标不连续,相对位置不可改变
- 例如
- [1,2,3,4,5] -> [1,3,5]
- 子数组(子串)
- 元素在原数组的下标连续,相对位置不可变
- 例如
- [1,2,3,4,5] -> [1,2,3]
特别的,子数组(子串)是子序列的一种特殊形式(当恰好取出的子序列是一连串的相邻元素时)
子序列常见问题与解法
-
最长上升子序列(LIS, Longest Increasing Subsequence)
子序列中的元素严格单调递增,即
nums[i-1] < nums[i]
总是成立LIS可能有多个,但是LIS的长度是固定的
- [1, 7, 3, 5, 9, 4, 8]
- res1 = [1, 3, 5, 8]
- res2 = [1, 3, 5, 9]
-
最长不升子序列
子序列中的元素总是满足
nums[i-1] >= nums[i]
-
最长下降子序列
子序列中的元素总是满足严格单调递减
nums[i-1] > nums[i]
-
最长不降子序列
子序列中的元素总是满足
nums[i-1] <= nums[i]
动态规划 - O(N^2)
定义dp数组
dp[i]
表前 i 个数以nums[i]
结尾的最长上升子序列长度base case
dp[i] = 1
状态转移
寻找左侧比 nums[i] 小的元素 nums[j], 在其基础上+1,没找到则维持1
dp[i] = max(dp[i], dp[j] + 1), (forEach j < i : nums[i] > nums[j])
// nums!=null && nums.length > 0
public int lis(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
// base case
Arrays.fill(dp, 1);
// find max
int max = 1;
// dp
for (int i = 1; i < n; i++) {
for (int j = i-1; j >= 0; j--) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
max = Math.max(max, dp[i]);
}
return max;
}
贪心+二分O(NlogN)
参考题解
核心策略
将问题转化为单调问题,利用二分解决单调问题
int[] tails = new int[n+1];
tails[i]
表示所有长度为 i 的 LIS ,其末尾元素的最小值PS:如果是递减趋势的问题,则改为末尾元素最大值即可
显然 tails[] 单调递增, tails[i-1] < tails[i]
int res 记录LIS长度,初始时 res <-1
更新规则
当 curr = nums[i] > tails[res]: tails[++res] = curr
否则 寻找第一个 tails[j] > curr 令 tails[j]<-curr
最终的结果res就是LIS长度,tails维护了各长度LIS的尾部(最右侧)最小值
// nums!=null && nums.length > 0
public int lis(int[] nums) {
int n = nums.length;
int[] tails = new int[n+1];
int res = 1; // 记录 LIS 长度
tails[res] = nums[0]; // 初始化,将 nums[0] 作为 res=1 的LIS末尾元素
int curr; // 记录遍历的值
for (int i = 1; i < n; i++) {
if ((curr = nums[i]) > tails[res]) {
tails[++res] = curr;
} else {
// 二分搜索第一个 tails[j] > curr
// 搜索区间 [lo, hi)
// tails[] 单调递增
int lo = 0, hi = res;
int mid;
while (lo < hi) {
mid = lo + ((hi-lo) >> 1); // 注意位运算优先级低于+-
if (tails[mid] == curr) {
lo = mid + 1;
} else if (tails[mid] < curr) {
lo = mid + 1;
} else { // tails[mid] > hi
hi = mid;
}
}// lo 就是要找的边界
tails[lo] = curr;
}
}
return res;
}
Dilworth定理
笔者实在看不懂,这里列出参考,直接给出结论
子序列的数量和子序列的长度存在对应关系
- 最长不升(>=)子序列的长度 = 最小上升(<)子序列的个数
- 最长上升(<)子序列的长度 = 最小不升(>=)子序列的个数
- 最长不降(<=)子序列的长度 = 最小下降(>)子序列的个数
- 最长下降(>)子序列的长度 = 最小不降(<=) 子序列的个数
该定理用于求解 满足条件的子序列的个数
例题
子数组(子串)的常见问题与解法
遇到的有
-
两个数组的最长公共部分
-
两个字符串的最长公共子串
动态规划
以718. 最长重复子数组为例,寻找两个一维数组的最长公共子数组
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
[0,1,1,1,1]
[1,0,1,0,1]
2
不同于子序列的动态规划解法,子数组(子串)具备连续性,与前面的不连续则需要断开连续
分析如下:
对于子串 nums1[0...i] 和 nums[0...j]
判断
nums1[i] == nums2[j] :
-
如果 nums1[i-1] == nums2[j-1] 则 基于这一 子串长度+1
-
否则 子串长度为 1
nums1[i] != nums2[j] :
- 子串长度为0
需要一个变量记录这一过程中的最大值
// 时间复杂度 O(n1*n2)
// nums1 != null && nums1.length > 0
// nums2 != null && nums2.length > 0
public int findLength(int[] nums1, int[] nums2) {
int n1 = nums1.length, n2 = nums2.length;
int[][] dp = new int[n1][n2];
// 记录最大公共长度
int max = 0;
// base case
// 第 0 行
for (int j = 0; j < n2; j++) {
dp[0][j] = nums1[0] == nums2[j]? 1: 0;
max = Math.max(max, dp[0][j]);
}
// 第 0 列
for (int i = 1; i < n1; i++) {
dp[i][0] = nums1[i] == nums2[0]? 1: 0;
max = Math.max(max, dp[i][0]);
}
// dp
for (int i = 1; i < n1; i++) {
for (int j = 1; j < n2; j++) {
if (nums1[i] == nums2[j]) {
dp[i][j] = dp[i-1][j-1] > 0? dp[i-1][j-1] + 1: 1;
}// else dp[i][j] = 0
max = Math.max(max, dp[i][j]);
}
}
return max;
}