LeetCode - Medium - 1143. Longest Common Subsequence

Topic

  • Dynamic Programming

Description

https://leetcode.com/problems/longest-common-subsequence/

Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0.

A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

For example, "ace" is a subsequence of "abcde". A common subsequence of two strings is a subsequence that is common to both strings.

Example 1:

Input: text1 = "abcde", text2 = "ace" 
Output: 3  
Explanation: The longest common subsequence is "ace" and its length is 3.

Example 2:

Input: text1 = "abc", text2 = "abc"
Output: 3
Explanation: The longest common subsequence is "abc" and its length is 3.

Example 3:

Input: text1 = "abc", text2 = "def"
Output: 0
Explanation: There is no such common subsequence, so the result is 0.

Constraints:

  • 1 <= text1.length, text2.length <= 1000
  • text1 and text2 consist of only lowercase English characters.

Analysis

方法一:標準DP

動態規劃五部曲:

  1. 確定dp數組(dp table)以及下標的含義;
  2. 確定遞推公式;
  3. dp數組如何初始化;
  4. 確定遍歷順序;
  5. 舉例推導dp數組。

DP數組意,歸納遞推式,初始元素值,定序來遍歷,最後舉個例

用動規五部曲分析如下

1.確定dp數組(dp table)以及下標的含義

dp[i][j]表示 從text1的下標0開始截取長度爲i的字符串 與 從text2的下標0開始截取長度爲j的字符串 的最長公共子序列的長度。

也可以換句話說。

dp[i][j]表示 從text1的下標0開始截取到下標i-1截取的字符串(包含下標i-1處的字符) 與 從text2的下標0開始截取到下標j-1截取的字符串(包含下標j-1處的字符) 的最長公共子序列的長度。

2.確定遞推公式

主要就是兩大情況:text1[i - 1] 與 text2[j - 1]相同,text1[i - 1] 與 text2[j - 1]不相同

  1. 如果text1[i - 1] 與 text2[j - 1]相同,那麼找到了一個公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
  2. 如果text1[i - 1] 與 text2[j - 1]不相同,那就看看text1[0, i - 2]與text2[0, j - 1]的最長公共子序列 和 text1[0, i - 1]與text2[0, j - 2]的最長公共子序列,取最大的,即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

代碼如下:

if (text1[i - 1] == text2[j - 1]) {
    dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}

3.dp數組如何初始化

先看看dp[i][0]應該是多少呢?

test1[0, i-1]和空字符串的最長公共子序列自然是0,所以dp[i][0] = 0;

同理dp[0][j]也是0。

其他下標都是隨着遞推公式逐步覆蓋,初始值是默認的即可。

總之,全部都是0,即默認的。

int[][] dp = new int[text1.length() + 1][text2.length() + 1];

4.確定遍歷順序

從遞推公式,可以看出,有三個方向可以推出dp[i][j],如圖:

在這裏插入圖片描述

那麼爲了在遞推的過程中,這三個方向都是經過計算的數值,所以要從前向後,從上到下來遍歷這個矩陣。

5.舉例推導dp數組

以輸入:text1 = "abcde", text2 = "ace" 爲例,dp狀態如圖:

在這裏插入圖片描述

最後紅框dp[text1.length()][text2.length()]爲最終結果。

再次重申,dp[i][j]表示 從text1的下標0開始截取長度爲i的字符串 與 從text2的下標0開始截取長度爲j的字符串 的最長公共子序列的長度。

方法二:方法一的空間優化

進一步地觀察,方法一的代碼遍歷時只需前一行以及目前一行的信息即可。因此,dp數組初始化兩行的

注意,用k ^ 1、k ^= 1來切換 dp[0] (第一行) 與 dp[1] (第二行)。

注意,m % 2 與 m & 1 意同。

方法三:方法一的空間優化Plus

再進一步地觀察,方法一的代碼遍歷時只需前一行以及目前一行的前一元素信息即可。因此,dp數組初始化一行的,另加3個變量進行輔助。

最後,優化後的時空雜度分別爲:O(m * n),O(min(m, n))。

參考資料

  1. 動態規劃:最長公共子序列
  2. <a href="https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis">[Java/Python 3] Two DP codes of O(mn) & O(min(m, n)) spaces w/ picture and analysis</a>

Submission

public class LongestCommonSubsequence {
	// 方法一:標準DP
	public int longestCommonSubsequence(String text1, String text2) {

		int[][] dp = new int[text1.length() + 1][text2.length() + 1];

		for (int i = 1; i &lt;= text1.length(); i++) {
			for (int j = 1; j &lt;= text2.length(); j++) {
				if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
					dp[i][j] = dp[i - 1][j - 1] + 1;
				} else {
					dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
				}
			}
		}

		return dp[text1.length()][text2.length()];
	}

	// 方法二:方法一的空間優化
	public int longestCommonSubsequence2(String s1, String s2) {
		int m = s1.length(), n = s2.length();
		if (m &lt; n)// 本判斷語句目的可以減少空間複雜度。可以移除本判斷語句。
			return longestCommonSubsequence2(s2, s1);
		int[][] dp = new int[2][n + 1];
		for (int i = 1, k = 1; i &lt;= m; ++i, k ^= 1)
			for (int j = 1; j &lt;= n; ++j)
				if (s1.charAt(i - 1) == s2.charAt(j - 1))
					dp[k][j] = 1 + dp[k ^ 1][j - 1];
				else
					dp[k][j] = Math.max(dp[k ^ 1][j], dp[k][j - 1]);
		return dp[m &amp; 1][n];
	}

	// 方法三:方法一的空間優化Plus
	public int longestCommonSubsequence3(String text1, String text2) {
		int m = text1.length(), n = text2.length();
		if (m &lt; n) {// 本判斷語句目的可以減少空間複雜度。可以移除本判斷語句。
			return longestCommonSubsequence3(text2, text1);
		}

		int[] dp = new int[n + 1];
		for (int i = 1; i &lt;= text1.length(); ++i) {
			for (int j = 1, left = 0, leftUp = 0, newOne = 0; j &lt;= text2.length(); ++j) {

				if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
					newOne = leftUp + 1;
				} else {
					newOne = Math.max(left, dp[j]);
				}
				leftUp = dp[j];
				left = dp[j] = newOne;// 爲同行下一元素以及下行元素做準備

			}
		}
		return dp[n];
	}
}

Test

import static org.junit.Assert.*;
import org.junit.Test;

public class LongestCommonSubsequenceTest {

	@Test
	public void test() {
		LongestCommonSubsequence obj = new LongestCommonSubsequence();

		assertEquals(3, obj.longestCommonSubsequence("abcde", "ace"));
		assertEquals(3, obj.longestCommonSubsequence("ace", "abcde"));
		assertEquals(3, obj.longestCommonSubsequence("abc", "abc"));
		assertEquals(0, obj.longestCommonSubsequence("abc", "def"));
	}

	@Test
	public void test2() {
		LongestCommonSubsequence obj = new LongestCommonSubsequence();

		assertEquals(3, obj.longestCommonSubsequence2("abcde", "ace"));
		assertEquals(3, obj.longestCommonSubsequence2("ace", "abcde"));
		assertEquals(3, obj.longestCommonSubsequence2("abc", "abc"));
		assertEquals(0, obj.longestCommonSubsequence2("abc", "def"));
	}

	@Test
	public void test3() {
		LongestCommonSubsequence obj = new LongestCommonSubsequence();

		assertEquals(3, obj.longestCommonSubsequence3("abcde", "ace"));
		assertEquals(3, obj.longestCommonSubsequence3("ace", "abcde"));
		assertEquals(3, obj.longestCommonSubsequence3("abc", "abc"));
		assertEquals(0, obj.longestCommonSubsequence3("abc", "def"));
	}

	@Test
	public void testOther() {
		assertEquals(1 &amp; 1, 1 % 2);
		assertEquals(2 &amp; 1, 2 % 2);
		assertEquals(3 &amp; 1, 3 % 2);
		assertEquals(4 &amp; 1, 4 % 2);
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章