动态规划:编辑距离和最长公共子序列解法对比

本文比较了LeetCode中的编辑距离和最长公共子序列问题中,dp函数的不同定义,并探讨了两种定义的等效性,以及它们在Python中的适用性,揭示了自底向上和自顶向下动态规划策略的区别。
摘要由CSDN通过智能技术生成

问题

编辑距离(leetcode 题号72)和最长公共子序列(leetcode 1143)是两道经典的动态规划问题,题目如下图所示。

在这里插入图片描述
在这里插入图片描述

解法对比

跟着labuladong刷题时,这两道里dp函数的传参一样,都是:

dp(word1: str, word2: str, i: int, j: int)

但是在对函数的定义上却不同:一个是返回将word1[:i]转化成word2[:j]所使用的最少操作数,另一个是返回text1[i:]和text2[j:]的最长公共子序列注意i和j,一个是作为左侧边界,一个是作为右侧边界定义的。

# 编辑距离
class Solution:
    def __init__(self):
        self.memo = dict()

    def dp(self, word1: str, word2: str, i: int, j: int):
        """"返回将word1[:i+1]转化成word2[:j+1]所使用的最少操作数"""
        # badcase,某一个指针提前走完了
        if i == -1:
            return j + 1
        if j == -1:
            return i + 1
        if (i, j) in self.memo:
            return self.memo[(i, j)]
        if word1[i] == word2[j]:
            self.memo[(i, j)] = self.dp(word1, word2, i - 1, j - 1)
        else:
        	# 这里隐藏的是:两个指针是从右往左的
            self.memo[(i, j)] = min(self.dp(word1, word2, i, j - 1) + 1,
                                    self.dp(word1, word2, i - 1, j) + 1,
                                    self.dp(word1, word2, i - 1, j - 1) + 1
                                    )

        return self.memo[(i, j)]

    def minDistance(self, word1: str, word2: str) -> int:
        """自顶向下:递归解法,DP函数"""
        m, n = len(word1), len(word2)
		# 为什么不是m, n而是m-1, n-1
        return self.dp(word1, word2, m - 1, n - 1)
# 最长公共子序列
class Solution_m():
    def __init__(self):
        self.memo = dict()

    def dp(self, text1: str, text2: str, i: int, j: int):
        """"返回text1[i:]和text2[j:]的最长公共子序列"""
        m, n = len(text1), len(text2)
        # badcase: 越界
        if i > m-1 or j > n-1:
            return 0
        if (i, j) in self.memo:
            return self.memo[(i, j)]
        if text1[i] == text2[j]:
        	# 这里隐藏的是,两个指针是从左往右的
            self.memo[(i, j)] = 1 + self.dp(text1, text2, i+1, j+1)
        else:
            self.memo[(i, j)] = max(self.dp(text1, text2, i+1, j),
                                    self.dp(text1, text2, i, j+1)
                                    )

        return self.memo[(i, j)]

    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        return self.dp2(text1, text2, 0, 0)

跟着东哥的思路写确实可以提交通过,但是我没有想明白,为啥要这么定义啊?这两个dp函数定义成一样的意思能不能行啊?

(啰嗦几句,要是按照我以前应付面试的刷题心态,这种问题就略过了,管他呢,没那个脑子想了,能通过就行了!但是今年听了武志红老师《把事情做好的心理学课》后,不想把刷leetcode仍然当作是一个痛苦、应付差事的事情,所以这种问题我感觉挺有意思的,就停下来,不赶进度了,好好的琢磨琢磨~让刷题过程变得有趣起来。)

思考

先画两张图,理解下东哥给的思路:
在这里插入图片描述
如果将编辑距离的DP函数的定义改一下能不能行?

现在是:返回将word1[:i+1]转化成word2[:j+1]所使用的最少操作数;
状态转移函数是:

if word1[i] == word2[j]:
    self.memo[(i, j)] = self.dp(word1, word2, i - 1, j - 1)
else:
    self.memo[(i, j)] = min(self.dp(word1, word2, i, j - 1) + 1,
                            self.dp(word1, word2, i - 1, j) + 1,
                            self.dp(word1, word2, i - 1, j - 1) + 1
                            )

我们改成:返回将word1[i:]转化成word2[j:]所使用的最少操作数,也就是和最长公共子序列一样的定义。 试试看!

如果DP函数定义变了,那么状态转移函数也要变:

if word1[i] == word2[j]:
    self.memo[(i, j)] = self.dp(word1, word2, i + 1, j + 1)
else:
    self.memo[(i, j)] = min(self.dp(word1, word2, i, j + 1) + 1,
                            self.dp(word1, word2, i + 1, j) + 1,
                            self.dp(word1, word2, i + 1, j + 1) + 1
                            )

那么相应的bad case也要从:

 # badcase,某一个指针提前走完了
if i == -1:
    return j + 1
if j == -1:
    return i + 1

更新为:

 # badcase,某一个指针提前走完了
if i == len(word1):
    return len(word2) - j
if j == len(word2):
    return len(word1) - i

完整代码为:

class Solution_o:
    def __init__(self):
        self.memo = dict()

    def dp(self, word1: str, word2: str, i: int, j: int):
        """"返回将word1[:i]转化成word2[:j]所使用的最少操作数"""
        # badcase,某一个指针提前走完了
        if i == -1:
            return j + 1
        if j == -1:
            return i + 1
        if (i, j) in self.memo:
            return self.memo[(i, j)]
        if word1[i] == word2[j]:
            self.memo[(i, j)] = self.dp(word1, word2, i - 1, j - 1)
        else:
            self.memo[(i, j)] = min(self.dp(word1, word2, i, j - 1) + 1,
                                    self.dp(word1, word2, i - 1, j) + 1,
                                    self.dp(word1, word2, i - 1, j - 1) + 1
                                    )

        return self.memo[(i, j)]

    def dp(self, word1: str, word2: str, i: int, j: int):
        """"返回将word1[i:]转化成word2[j:]所使用的最少操作数"""
        # badcase,某一个指针提前走完了
        if i == len(word1):
            return len(word2) - j
        if j == len(word2):
            return len(word1) - i
        if (i, j) in self.memo:
            return self.memo[(i, j)]
        if word1[i] == word2[j]:
            self.memo[(i, j)] = self.dp(word1, word2, i + 1, j + 1)
        else:
            self.memo[(i, j)] = min(self.dp(word1, word2, i, j + 1) + 1,
                                    self.dp(word1, word2, i + 1, j) + 1,
                                    self.dp(word1, word2, i + 1, j + 1) + 1
                                    )

        return self.memo[(i, j)]

    def minDistance(self, word1: str, word2: str) -> int:
        """自顶向下:递归解法,DP函数"""
        m, n = len(word1), len(word2)
        # 这里也有不同
        #return self.dp(word1, word2, m-1, n-1)
        return self.dp2(word1, word2, 0, 0)

结果也是对的!说明这两种定义是可以互通的!

总结

以下两种定义方式都行,看个人习惯。

我个人更喜欢右边的方式!因为python中数组的切片word[i:j]是左闭右开区间,用dp(i, j)表示从word1[i:]转化为word2[j:]的定义更简洁。
在这里插入图片描述

另一个延伸思考,DP的题目什么时候用DP数组,什么时候用DP函数?

DP数组是自底向上的写法,因为这种写法要求你算当前的dp[i][j]的时候,它所依赖的状态函数已经被求出来了。而DP函数的写法是一种自顶向下的写法,一般配合备忘录来写,就是算当前dp(i, j)时,它所依赖的状态不一定已经算出来的,但是通过递归最终能求出,又因为有备忘录,所以可以避免重复计算。

参考资料

  1. 《labuladong的算法小抄》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值