LeetCode718 最长重复子数组(带扩展)

124 阅读4分钟

leetcode.cn/problems/ma…

image.png

解法一:暴力法(居然也能通过...)

算法的本质是穷举,先写一个暴力解做到无遗漏地穷举,最容易想到的解法如下

func findLength(nums1 []int, nums2 []int) int {
    res := 0
    for i := 0; i<len(nums1); i++{
        for j:=0; j<len(nums2); j++{
            // 统计以nums1[i]和nums2[j]为起点的重复子数组长度
            tmp := 0 
            p1, p2 := i, j
            for p1 < len(nums1) && p2 < len(nums2) && nums1[p1] == nums2[p2]{
                tmp++
                p1++
                p2++
            }
            // 找到一个重复子数组,更新答案
            res = max(res, tmp)
        }
    }
    return res
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}

时间复杂度:O(N^3) 空间复杂度:O(1)

解法二:自底向上的递推DP(二维动态规划)

只要遇到公共前缀/后缀这类问题,大概率有冗余计算,要往动态规划的思路上靠。

前面的暴力法中哪里可以进一步优化呢?

最内层的 for 循环可以用空间换时间的思路优化掉,构建一个二维dp数组

dp[i][j] 表示以nums1[i-1]结尾的子数组和以nums2[j-1]结尾的子数组的最大公共子数组长度。长度+1是为了兼容空数组情况,例如,其中一个数组为空,或者两个空数组,都不存在重复子数组。即dp[0][j]和dp[i][0]都为0。

  • 如果nums1[i] == nums1[j], dp[i+1][j+1] = dp[i][j] + 1
  • 如果nums1[i] != nums1[j], dp[i+1][j+1] = 0 (就是申请数组的初始值,什么都不用改)
func findLength(nums1 []int, nums2 []int) int {
    // dp[i][j] 表示以nums1[i-1]结尾的子数组和以nums2[j-1]结尾的子数组的最大公共子数组长度
    dp := make([][]int, len(nums1)+1)
    for idx := range dp{
        dp[idx] = make([]int, len(nums2)+1)
    }
    res := 0
    for i := 1; i<len(dp); i++{
        for j := 1; j<len(dp[0]); j++{
            if nums1[i-1] == nums2[j-1]{
                dp[i][j] = dp[i-1][j-1] + 1
                res = max(res, dp[i][j])
            }
        }
    }
    return res
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}

时间复杂度:O(N^2)

空间复杂度:O(n*m),二维dp数组占用的空间,其中 n 是 nums1的长度,m 是 nums2的长度

解法三:自顶向下的递归dp + 备忘录

动态规划一般都有两种实现方案,我们再尝试用递归的思想实现一下,注意为了避免暴力穷举出现的冗余计算,我们可以借助一个备忘录。

func findLength(nums1 []int, nums2 []int) int {
    memo := make([][]int, len(nums1))
    // 备忘录初始化为一个和可能答案不冲突的值
    for i := range memo{
        memo[i] = make([]int, len(nums2))
        for j := range memo[i]{ 
            memo[i][j] = -1
        }
    }
    res := 0
    for i := 0; i < len(nums1); i++{
        for j := 0; j<len(nums2); j++{
            res = max(res, dp(nums1, i, nums2, j, memo))
        }
    }
    return res
}

// dp函数返回nums1[...i]和nums2[...j]的最长重复子数组长度
func dp(nums1 []int, i int, nums2 []int, j int, memo [][]int) int{
    if i < 0 || j < 0{ // base case,有一个为空数组即无重复子数组了
        return 0
    }
    if memo[i][j] != -1{ // 避免重复计算
        return memo[i][j]
    }
    if nums1[i] == nums2[j]{
        memo[i][j] = dp(nums1, i-1, nums2, j-1, memo) + 1
    }else{
        memo[i][j] = 0
    }
    return memo[i][j]
}

func max(a, b int) int{
    if a > b{
        return a
    }
    return b
}

扩展:百度面试真题

找到两个字符串的最长公共子串

eg: 字符串1"abcdagh", 字符串2"abfagh", 最长公共子串是"agh"

分析

其实字符串也是一个字符数组,因此本质上和寻找重复子数组是类似的,只不过我们需要记录的答案不是长度,而是这个子串。

解法一:暴力法
func main() {
	s1 := "abcdagh"
	s2 := "abfagh"
	res := findCommonStr(s1, s2)
	fmt.Println(res)
}

func findCommonStr(s1, s2 string) string {
	res := ""
	for i := range s1 {
		for j := range s2 {
			tmp := ""
			p1, p2 := i, j
			for p1 < len(s1) && p2 < len(s2) && s1[p1] == s2[p2] {
				tmp += string(s1[p1])
				p1++
				p2++
			}
			if len(tmp) > len(res) {
				res = tmp
			}
		}
	}
	return res
}
解法二:自底向上的dp

dp数组里头存放的元素改为是公共子串即可

func main() {
	s1 := "abcdagh"
	s2 := "abfagh"
	res := findCommonStr(s1, s2)
	fmt.Println(res)
}

func findCommonStr(s1, s2 string) string {
	// dp[i][j] 记录s1[:i]和s2[:j]的最长公共子串
	// 长度+1是为了兼容空串的情况,dp[0][j]和dp[i][0]一定为0
	dp := make([][]string, len(s1)+1)
	for i := range dp {
		dp[i] = make([]string, len(s2)+1)
	}
	res := ""
	for i := range s1 {
		for j := range s2 {
			if s1[i] == s2[j] {
				dp[i+1][j+1] = dp[i][j] + string(s1[i])
				if len(dp[i+1][j+1]) > len(res) {
					res = dp[i+1][j+1]
				}
			}
		}
	}
	return res
}