题目链接
https://leetcode.com/problems/maximum-length-of-repeated-subarray/
题目描述
给定两个整数数组A和B,返回两个数组的最长公共子数组的长度。(最长公共子串问题)
示例
输入:
A:[1,2,3,2,1]
B:[3,2,1,4,7]
输出:3
最长公共子数组为[3,2,1]
解决思路一
可以计算多对子数组的最长公共前缀长度(最长公共子数组以哪一项为起始项都有可能)或最长公共后缀长度(最长公共子数组以哪一项为末尾项都有可能),最终求最大值得到两个数组的最长公共子数组的长度。
此题满足动态规划算法的三个适用条件:
(1)求一个问题的最优解;
(2)问题能被划分为若干个子问题,这些子问题之间还有相互重叠的更小的子问题;(可以通过遍历给定的两个数组来划分子数组,对子数组的计算又包括了更短长度子数组的计算)
(3)该问题的最优解依赖于子问题的最优解。(可以计算划分得到的多对子数组的最长公共前缀长度,求最大值得到结果。)
因此这个问题可以用动态规划来解决。
(1)第一种更新方式:定义dp[i][j]为A[i:]和B[j:]的最长公共前缀长度,其中A[i:]表示从A[i]到A[-1]区间构成的子数组,B[j:]表示从B[j]到B[-1]区间构成的子数组。二维数组将从右下角到左上角更新。如果A[i] == B[j],那么更新dp[i][j] = dp[i+1][j+1] + 1,即在A[i+1:]和B[j+1:]的最长公共前缀的基础上增加元素。如果A[i] != B[j],那么dp[i][j] = 0。Python实现代码如下:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
m,n = map(len,(A,B))
dp = [[0 for _ in range(n+1)] for _ in range(m+1)] #动态规划表的大小设置为(m+1)*(n+1),因为后续dp[m-1][n-1]的更新要用到dp[m][n]
max_length = 0
for i in range(m-1,-1,-1):#i取值范围为[m-1,-1)
for j in range(n-1,-1,-1):#j取值范围为[n-1,-1)
if A[i] == B[j]: #dp[i][j]被初始化为0,因此如果A[i] != B[j]则不做任何处理
dp[i][j] = dp[i+1][j+1] + 1
max_length = max(max_length,dp[i][j])
return max_length
(2)如果计算子数组的最长公共后缀长度,定义dp[i][j]为A[:i](从A[0]到A[i-1])和B[:j](从B[0]到B[j-1])的最长公共后缀长度。如果A[i-1] == B[j-1],那么dp[i][j] = dp[i-1][j-1] + 1,如果A[i-1] != B[j-1],那么设置dp[i][j] = 0。二维数组将从左上角到右下角更新。Python实现代码如下:
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
m,n = map(len,(A,B))
dp = [[0 for _ in range(n+1)] for _ in range(m+1)] #一般动态规划表的大小设置为(m+1)*(n+1)
max_length = 0
for i in range(1,m+1):#i取值范围为[1,m],表示A数组的索引取值范围为[0,m-1]
for j in range(1,n+1):#j取值范围为[1,n],表示B数组的索引取值范围为[0,n-1]
if A[i-1] == B[j-1]: #dp[i][j]被初始化为0,因此如果A[i] != B[j]则不做任何处理
dp[i][j] = dp[i-1][j-1] + 1
max_length = max(max_length,dp[i][j])
return max_length
上面两种动态规划算法的时间复杂度为O(mn),空间复杂度为O(mn)。
空间复杂度还可以进一步减小,因为dp[i][j]仅跟dp[i+1][j+1]或者dp[i-1][j-1]有关,可以进一步只用一维数组来存储状态。
解决思路二
通过观察发现,重复子数组在两个数组中的位置可能不同。那么我们知道重复子数组的开始位置后,就可以据此将数组A和B进行对齐,然后从首个对齐元素开始,对两个数组进行遍历比较从而得到重复子数组的长度。A B两个数组所有对齐方式包括两种:
(1)A不动,B移动使数组B的首个元素B[0]逐个和A中的元素对齐;
(2)B不动,A移动使数组A的首个元素A[0]逐个和B中的元素对齐。
举例如数组A:[1,2,3]和B:[4,5,6,7],两个数组的对齐方式有以下几种:
橙色元素为首个对齐元素,红色框框住的是需要逐个比较的元素。
有一个优化的点在于,如果待比较的元素个数(红框中的元素个数)小于存放当前最长重复子数组长度的tmp_max,可以直接跳出当前循环。因为当前循环计算得到的重复子数组长度小于等于红框中待比较的元素个数,因此一定小于tmp_max,同时随着i的增长,length会越来越小,因此可以直接用break语句。
Python实现
class Solution:
def findLength(self, A: List[int], B: List[int]) -> int:
def getTmpCommon(start_A,start_B,length):
tmp_max = count = 0
for i in range(length):
if(A[start_A + i] == B[start_B + i]):
count += 1
tmp_max = max(tmp_max,count)
else:
count = 0 #中间有一个元素不同,count需要重置为0,但是当前最大连续重复元素个数保存在tmp_max中
return tmp_max
m,n = len(A),len(B)
tmp_max = 0
for i in range(m): #B不动,A移动对齐。A[i]和B[0]为首对齐元素
length = min(m-i,n)#对齐长度为m-i,n中的最小值(红框中的元素个数)
if(length < tmp_max): #如果待比较红框里的元素个数小于tmp_max,那接下来就没有比较的意义了,因为getTmpCommon得到的结果一定比tmp_max小,并且随着i增大,红框里的元素会越来越少。因此用break而不是continue
break
tmp_max = max(tmp_max,getTmpCommon(i,0,length))#更新tmp_max
for i in range(n):#A不动,B移动对齐。B[i]和A[0]为首对齐元素
length = min(n-i,m)#对齐长度为n-i,m中的最小值(红框中的元素个数)
if(length < tmp_max): #如果待比较红框里的元素个数小于tmp_max,那这一轮就没有比较的意义了,因为getTmpCommon得到的结果一定比tmp_max小。
break
tmp_max = max(tmp_max,getTmpCommon(0,i,length))
return tmp_max
思路二的时间复杂度为O((m+n)*min(m,n)),空间复杂度为O(1)。
解决思路三
此外还有结合二分查找与哈希的解决思路,能够进一步降低时间复杂度。可看参考中的官方题解,有时间会在博文里再描述。
参考