LeetCode – 583. Delete Operation for Two Strings

Given two words word1 and word2, find the minimum number of steps required to make word1 and word2 the same, where in each step you can delete one character in either string.

Example 1:

Input: "sea", "eat"
Output: 2
Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".

Note:

  1. The length of given words won't exceed 500.
  2. Characters in given words can only be lower-case letters.

题意:

给你两个单词word1和word2,请问至少需要几次删除操作使得word1和word2变得一样?每一步你都可以从word1或者word2里删除一个字符。

例如,如果输入两个单词"sea"和"eat",我们至少需要两步删除操作,分别删除第一个单词的's'和第二个单词的't',使得它们变成相同的"ea"。

分析:

如果你看过 LeetCode – 718. Maximum Length of Repeated Subarray 这道题,就会发现,这是一道求最长公共子串(LCS, Longest Common Subsequence)问题。

我们的目标是删除一些字符之后,word1和word2相同,也就是剩下的是两个字符串的公共子串。剩下的公共子串越长,那么需要的删除操作就越少。

于是,我们可以写出如下的函数:

public int minDistance(String word1, String word2) {
	int len = longestCommonSubsequenceGood(word1, word2);
	return word1.length()-len + word2.length()-len;
}

接下来我们来解决其中的关键问题,也就是如果求两个字符串的最长公共子串。

在 LeetCode – 718. Maximum Length of Repeated Subarray 这道题中,我只是提了一种比较普遍的解决方法。下面先来回忆一下这个方法,然后我再介绍一种空间复杂度更加优化的方法。

空间效率O(n^2)的解法:

由于这是一个求解最优解的问题(“最长”公共子串),我们可以尝试应用动态规划。应用动态规划的第一步找出状态转移函数。我们用函数f(i,j)表示第一个字符串s1的前i个字符组成的子字符串和第二个字符串s2的前j个字符组成的子字符串的最长公共子串的长度。

我们分两种情况讨论这个函数:

1. 如果s1中的第i个字符和s2中的第j个字符相同,f(i,j)等于f(i-1,j-1)+1。这相当于在s1的前i-1个字符组成的子字符串和s2的前j-1个字符组成的子字符串的最长公共子串的基础上增加了一个公共的字符。

2. 如果s1中的第i个字符和s2中的第j个字符不同,f(i,j)等于f(i-1,j)和f(i,j-1)的较大值。既然s1中的第i个字符和s2中的第j个字符不同,我们可以忽略s1中的第i个字符,去看看在s1的前i-1个字符组成的子字符串和s2的前j个字符组成的子字符串的最长公共子串的长度,这就是f(i-1,j)的值。同样,我们也可以忽略s2中的第j个字符,去看看在s1的前i个字符组成的子字符串和s2的前j-1个字符组成的子字符串的最长公共子串的长度,这就是f(i,j-1)的值。

由于状态转移函数有两个变量i和j,我们可以用一个二维矩阵来存储f(i,j)的值。以下就是基于二维矩阵的代码:

public int longestCommonSubsequence(String s1, String s2) {
	int[][] dp = new int[s1.length()+1][s2.length()+1];
	for (int i = 1; i <= s1.length(); i ++) {
		for (int j = 1; j <= s2.length(); j ++) {
			if (s1.charAt(i-1) == s2.charAt(j-1)) {
				dp[i][j] = dp[i-1][j-1] + 1;
			} else {
				dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
			}
		}
	}
	return dp[s1.length()][s2.length()];
}

如果两个字符串的长度分别是m和n,上述代码的时间和空间复杂度都是O(mn)。

空间效率O(n)的解法:

仔细分析上述代码,我们就能发现在求解dp[i][j]的时候只用到了dp[i-1][j-i]、dp[i-1][j]和dp[i][j-1],这三个值要么位于二维矩阵dp的第i-1行,要么位于dp的第i行。因此,求任意一个dp[i][j]的时候,只要矩阵中的第i-1行和第i行这两行就够了,并不是真正需要保留所有的m+1行(假设m为第一个字符串s1的长度)。这样空间复杂度就降低到O(n)了。

接下来我们看能不能进一步减少空间的使用,只保留二维矩阵dp中的一行,也就是只保留一个一维数组。如果只保留二维矩阵中的一行,第i-1行第j-1的数值(即f(i-1,j-1))和第i行j-1列的数值(即f(i,j-1))都对应到一维数组中的第j-1个数值。可是我们在求f(i,j)又同时需要第i-1行第j-1列的数值和第i行j-1列的数值,因此我们需要确保它们两个在使用之前不能相互覆盖。

我们注意到f(i-1,j-1)的值只是在求解f(i,j)有用,之后再也不需要。因此我们在求解f(i,j)的时候,先不把f(i,j-1)的值写入到一维数组,而是用一个临时变量保存。在求解f(i,j)之后,再把f(i,j-1)写入到一维数组。此时即使把f(i-1,j-1)的值覆盖,也不会有任何问题。

基于上面的优化,我们可以写出如下代码:

public int longestCommonSubsequenceGood(String s1, String s2) {
	int[] dp = new int[s2.length()+1];
	for (int i = 1; i <= s1.length(); i ++) {
		int prev = 0;
		int j = 1;
		for ( ; j <= s2.length(); j ++) {
			int cur = (s1.charAt(i-1) == s2.charAt(j-1))
					? dp[j-1] + 1
					: Math.max(dp[j], prev);
			dp[j-1] = prev;
			prev = cur;
		}
		dp[j-1] = prev;
	}
	return dp[s2.length()];
}

上述代码仍然需要两重循环,因此时间复杂度仍然是O(mn)。由于只需要一个一维数组,因此空间复杂度是O(n)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值