题目描述
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
动态规划
两个数组,找公共子数组。第一思路是动态规划,设f[i,j]
表示在nums1
中以位置i
结尾,nums2
中以位置j
结尾,的全部公共子数组中,长度最大的公共子数组的长度。
- 当
nums1[i] != nums2[j]
,很明显不存在公共子数组,故f[i,j] = 0
- 当
nums1[i] == nums2[j]
,存在公共子数组,f[i,j] = f[i - 1, j - 1] + 1
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
int[][] f = new int[n + 1][m + 1];
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
ans = Math.max(ans, f[i][j]);
}
}
}
return ans;
}
}
优化:观察可以知道f[i][j]
的状态,只取决于其左上角的状态(上一行的左侧那个位置),则可以用滚动数组的思想,优化为一维。(每一行状态的更新,从右往左)
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
int[] f = new int[m + 1];
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 1; j--) {
if (nums1[i - 1] == nums2[j - 1]) {
f[j] = f[j - 1] + 1;
ans = Math.max(ans, f[j]);
} else {
f[j] = 0; // 需要还原为0
}
}
}
return ans;
}
}
动规的时间复杂度是 O ( n × m ) O(n × m) O(n×m),空间复杂度是 O ( m i n ( n , m ) ) O(min(n,m)) O(min(n,m))
滑动窗口
最长重复子数组,一定是将两个数组在某个位置上对齐,之后在重合的区间内,能够得到的最长的子数组。枚举所有的对齐方式,然后判断此时重合区间内的重复子数组的长度即可。
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
int ans = 0;
for (int i = m - 1; i >= 0; i--) {
ans = Math.max(ans, getMaxLen(nums1, 0, nums2, i));
}
for (int i = 1; i < n; i++) {
ans = Math.max(ans, getMaxLen(nums1, i, nums2, 0));
}
return ans;
}
private int getMaxLen(int[] nums1, int i, int[] nums2, int j) {
int n = nums1.length, m = nums2.length;
int len = 0, maxLen = 0;
while (i < n && j < m) {
if (nums1[i] == nums2[j]) {
len++;
} else {
maxLen = Math.max(maxLen, len); // 断掉了, 记录答案
len = 0; // 重置
}
i++;
j++;
}
// 最后一次的len
maxLen = Math.max(maxLen, len);
return maxLen;
}
}
时间复杂度 O ( ( n + m ) × m i n ( n , m ) ) O((n + m) × min(n,m)) O((n+m)×min(n,m)),空间复杂度 O ( 1 ) O(1) O(1)
二分 + 哈希
由于,当存在一个长度为k
的公共子数组时,那么一定会存在一个长度小于k
的子数组,那么我们可以使用二分,每次二分检查一下,两个数组中是否存在长度为len
的重复子数组,如果存在,则往右侧查找,若不存在,则往左侧查找。
如何快速判断两个数组中是否存在某个长度的重复子数组呢?使用类似字符串哈希的方式 -> 参考这篇文章。
class Solution {
final int P = 131;
int[] hashA, hashB;
int[] p; // 存P的幂
public int findLength(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
hashA = new int[n + 1];
hashB = new int[m + 1];
p = new int[Math.max(n, m) + 1];
// 预处理出所有的哈希值
p[0] = 1; // P^0 = 1
for (int i = 1; i < p.length; i++) {
p[i] = p[i - 1] * P;
}
for (int i = 1; i <= n; i++) {
hashA[i] = hashA[i - 1] * P + nums1[i - 1];
}
for (int i = 1; i <= m; i++) {
hashB[i] = hashB[i - 1] * P + nums2[i - 1];
}
// 开始二分
int l = 0, r = Math.min(n, m);
while (l < r) {
int mid = l + r + 1 >> 1; // 查看一下这个长度是否存在公共子数组
if (check(mid)) l = mid; // 若这个长度存在, 则答案在右侧
else r = mid - 1;
}
return l;
}
// 检查@len 这个长度, 是否存在公共子数组
private boolean check(int len) {
Set<Integer> set = new HashSet<>();
int n = hashA.length - 1;
int m = hashB.length - 1;
for (int i = 1; i + len - 1 <= n; i++) {
int h = hashA[i + len - 1] - hashA[i - 1] * p[len];
set.add(h);
}
for (int i = 1; i + len - 1 <= m; i++) {
int h = hashB[i + len - 1] - hashB[i - 1] * p[len];
if (set.contains(h)) return true;
}
return false;
}
}
时间复杂度: O ( ( n + m ) × l o g ( m i n ( m , n ) ) ) O((n + m) × log(min(m,n))) O((n+m)×log(min(m,n)))