LeetCode-中-1143。最長共通部分列

トピック

  • 動的計画法

説明

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

2つの文字列text1とが与えられた場合text2、それらの最長共通部分の長さを返します。共通のサブシーケンスがない場合は、を返し0ます。

文字列サブシーケンスは、元の文字列から生成された新しい文字列であり、残りの文字の相対的な順序を変更せずに、一部の文字(なしでもかまいません)が削除されます。

たとえば"ace"、はのサブシーケンスです"abcde"共通サブシーケンス2つの文字列は、両方の文字列に共通するサブシーケンスです。

例1

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

例2

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

例3

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

制約:

  • 1 <= text1.length, text2.length <= 1000
  • text1text2小文字の英字のみ構成されます。

分析

方法1:標準DP

動的計画への5つのステップ:

  1. dp配列(dpテーブル)と添え字の意味を決定します。
  2. 漸化式を決定します。
  3. dp配列を初期化する方法。
  4. 走査順序を決定します。
  5. dp配列を導出する例を挙げてください。

DP配列とは、帰納的再帰、初期要素値、トラバースするようにシーケンスされたものを意味し、最後に例を示します。

次のように、移動ルールの5つの部分からなる分析を使用します。

1. dp配列(dpテーブル)と添え字の意味を決定します

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.漸化式を決定します

主に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]は3つの方向で導出できることがわかります。

ここに画像の説明を挿入

したがって、再帰的に実行するには、これらの3つの方向はすべて計算値であるため、行列前から後ろ、上から下に移動する必要あります。

5.dp配列を導出する例

入力を取ります:例としてtext1 = "abcde"、text2 = "ace"、dpステータスは図に示すようになります:

ここに画像の説明を挿入

最後に、赤いボックスdp [text1.length()] [text2.length()]が最終結果です。

繰り返しになりますが、dp [i] [j]は、text1の添え字0からインターセプトされた長さiの文字列とtext2の添え字0からの長さjの文字列の最長共通部分列の長さを表します。

方法2:方法1のスペース最適化

さらに観察すると、方法1のコード走査には、前の行と現在の行の情報のみが必要です。したがって、dp配列は2行で初期化されます

k ^ 1、k ^ = 1は、dp [0](最初の行)とdp [1](2番目の行)を切り替えるために使用されることに注意してください。

m%2はm&1と同義であることに注意してください。

方法3:方法1スペース最適化プラス

さらに、方法1のコードをトラバースするときは、前の行と現在の行の前の要素情報だけで十分であることに注意してください。したがって、dp配列は1行初期化され、支援のために3つの変数が追加されます。

最後に、最適化された時空間クラッターは、O(m * n)、O(min(m、n))です。

参照

  1. 動的計画法:最長共通部分列
  2. [Java / Python 3] O(mn)およびO(min(m、n))スペースの2つのDPコード(画像と分析付き)

提出

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 <= text1.length(); i++) {
    
    
			for (int j = 1; j <= 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 < n)// 本判断语句目的可以减少空间复杂度。可以移除本判断语句。
			return longestCommonSubsequence2(s2, s1);
		int[][] dp = new int[2][n + 1];
		for (int i = 1, k = 1; i <= m; ++i, k ^= 1)
			for (int j = 1; j <= 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 & 1][n];
	}

	// 方法三:方法一的空间优化Plus
	public int longestCommonSubsequence3(String text1, String text2) {
    
    
		int m = text1.length(), n = text2.length();
		if (m < n) {
    
    // 本判断语句目的可以减少空间复杂度。可以移除本判断语句。
			return longestCommonSubsequence3(text2, text1);
		}

		int[] dp = new int[n + 1];
		for (int i = 1; i <= text1.length(); ++i) {
    
    
			for (int j = 1, left = 0, leftUp = 0, newOne = 0; j <= 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];
	}
}

テスト

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 & 1, 1 % 2);
		assertEquals(2 & 1, 2 % 2);
		assertEquals(3 & 1, 3 % 2);
		assertEquals(4 & 1, 4 % 2);
	}
}

おすすめ

転載: blog.csdn.net/u011863024/article/details/114928943