トピック
- 動的計画法
説明
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
text1
text2
小文字の英字のみで構成されます。
分析
方法1:標準DP
動的計画への5つのステップ:
- dp配列(dpテーブル)と添え字の意味を決定します。
- 漸化式を決定します。
- dp配列を初期化する方法。
- 走査順序を決定します。
- 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]と同じではありません。
- text1 [i-1]がtext2 [j-1]と同じである場合、共通の要素が見つかるので、
dp[i][j] = dp[i - 1][j - 1] + 1;
。 - 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))です。
参照
提出
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);
}
}