LeetCode 1143
刚看到这个题目,考虑了dp,但是一下没有想到简单的状态转移方法。所以开始考虑其他方法,首先想到的是递归求解,比如 “abcde” 和“ace", 我们可以把这个问题分成两个子问题,最后的“e"在最长公共字串中用到了,还有一种是这个e没有用到。这样我们可以不断的递归减短这个字符串,直到字符串长度为0.
写成递归的代码:
def longestCommonSubsequenceTimeout(self, text1: str, text2: str) -> int:
l1 = len(text1)
l2 = len(text2)
if l1 == 0 or l2 == 0: return 0
last = text2[-1]
try:
index = text1.rindex(last)
except:
#特殊case,最后一个字符在第一个字串中不存在,所以只有一种情况
return self.longestCommonSubsequence(text1, text2[:-1])
#分成用到了最后一个字符和没有用到最后一个字符两种情况
return max(self.longestCommonSubsequence(text1[0:index], text2[:-1]) + 1,
self.longestCommonSubsequence(text1, text2[:-1]))
这个效率还是比较低的,时间复杂度是指数级的。
所以提交之后time out 了。
那么需要优化,最直接的方法就是用memo,记住访问过的情况:
这个转换也很简单,用字典根据第一个字串的位置和第二个字串做key,记住访问过的情况。
def longestCommonSubsequenceMemo(self, text1: str, text2: str) -> int:
memo = collections.defaultdict(int)
def longestCommon(text1, text2, memo):
l1 = len(text1)
l2 = len(text2)
if l1 == 0 or l2 == 0: return 0
if (len(text1), text2) in memo:
return memo[(len(text1), text2)]
last = text2[-1]
try:
index = text1.rindex(last)
except:
memo[(len(text1), text2)] = longestCommon(text1, text2[:-1], memo)
return memo[(len(text1), text2)]
memo[(len(text1), text2)] = max(longestCommon(text1[0:index], text2[:-1], memo) + 1,
longestCommon(text1, text2[:-1], memo))
return memo[(len(text1), text2)]
return longestCommon(text1, text2, memo)
这个被接受了,但是时间只有5%。感觉还是不够好
所以重新回头考虑dp。如果用一个二维数组记住,到位置(i,j) 位置的最长公共字串,那么我们下一个i+1的字符串,只要从j的位置开始match,找到一个k的话,那么(i+1,k)的公共字串就是dp(i,j)+1.
如果找不到k,那么dp【i+1,j】= max(dp【i,j】,dp【i+1,j】)(可能被之前的设置过)
转为代码是:
def longestCommonSubsequencedp(self, text1: str, text2: str) -> int:
l1 = len(text1)
l2 = len(text2)
if l1 == 0 or l2 == 0: return 0
dp = [[-1] * (l1+1) for i in range(l2 + 1)]
dp[0][0] = 0
for i in range(1, l2+1):
for pos, v in enumerate(dp[i-1]):
if v != -1:
dp[i][pos] = max(dp[i][pos], v)
try:
index = text1.index(text2[i-1], pos)
except:
continue
dp[i][index+1] = max(dp[i][index+1], v + 1)
return max(dp[l2])
这个也被接受了,但是时间只beat了10%。每次要index效率还是不高。
到了这步,我们其实记录的dp【i,j】就是到i,j的最长公共字串。那么我么重新考虑dp的状态转移方式。i+1,j+1 的字符不match,那么其实最长字串要么就是(i,j+1)或者是(i+1,j)因为自己是没有match的。如果match了,本身已经被消耗掉了,那么之前最长的只能是(i,j)。转换为代码:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
l1 = len(text1)
l2 = len(text2)
if l1 == 0 or l2 == 0: return 0
dp = [[0] * (l1+1) for i in range(l2 + 1)]
#用了哨兵,省去了特殊处理第一行/列
for j, t2 in enumerate(text2):
for i, t1 in enumerate(text1):
if t1 != t2:
dp[j+1][i+1] = max(dp[j+1][i], dp[j][i+1])
else:
dp[j+1][i+1] = dp[j][i] + 1
return dp[l2][l1]
这个效率就还不错了,beat了87%