51Nod-1006-最長公共子序列LCS 和 最長公眾子串
阿新 • • 發佈:2019-01-07
51Nod-1006-最長公共子序列LCS 和 最長公眾子串
51Nod-1006-最長公共子序列LCS
題目連結
題目
就是輸入兩個字串str1
、str2
,輸出任意一個最長公共子序列。
解析
dp[i][j]
代表的是 : 必須以str1[i]
、str2[j]
結尾的最長公共子序列,dp[i][j]
來源:
- 可能是
dp[i-1][j]
,代表str1[0~i-1]
與str2[0~j]
的最長公共子序列。 - 可能是
dp[i][j-1]
,代表str1[0~i]
與str2[0~j-1]
的最長公共子序列。 - 如果
str1[i] == str2[j]
,還可能是dp[i-1][j-1] + 1
。
這三種情況中取最大值。
構造結果的過程(利用dp
陣列即可)
- 從矩陣的右下角開始,有三種移動方式: 向上、向左、向左上。
- 如果
dp[i][j] > dp[i-1][j] && dp[i][j] > dp[i][j-1]
,說明之前在計算dp[i][j]
的時候,一定是選擇了dp[i-1][j-1]+1
,所以可以確定str1[i] = str2[j]
,並且這個字元一定輸入最長公共子序列,把這個字元放進結果字串,然後向左上方移動; - 如果
dp[i][j] == dp[i-1][j]
dp[i][j]
的時候,dp[i-1][j-1]+1
不是必須的選擇,向 上方移動即可; - 如果
dp[i][j] == dp[i][j-1]
,向 左方移動即可; - 如果
dp[i][j]
同時等於dp[i-1][j]
和dp[i][j-1]
,向上向左都可以,選擇一個即可,不會錯過必須選擇的字元;
import java.io.BufferedInputStream;
import java.util.Scanner;
public class Main {
/** dp[i][j]代表的是 str[0..i]與str[0...j]的最長公共子序列*/
public static int[][] getDp(char[] sa,char[] sb){
int[][] dp = new int[sa.length][sb.length];
dp[0][0] = sa[0] == sb[0] ? 1 : 0;
for(int i = 1; i < sa.length; i++) // 一旦dp[i][0]被設定成1,則dp[i~N-1][0]都為1
dp[i][0] = Math.max(dp[i-1][0], sa[i] == sb[0] ? 1 : 0);
for(int j = 1; j < sb.length; j++)
dp[0][j] = Math.max(dp[0][j-1], sb[j] == sa[0] ? 1 : 0);
for(int i = 1; i < sa.length; i++){
for(int j = 1; j < sb.length; j++){
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
if(sa[i] == sb[j]){
dp[i][j] = Math.max(dp[i][j],dp[i-1][j-1] + 1);
}
}
}
return dp;
}
/*** 求出最長公共子序列*/
public static String getLCS(String sa, String sb, int[][] dp){
if(sa == null || sb == null || sa.equals("") || sb.equals(""))
return "";
char[] chs1 = sa.toCharArray();
char[] chs2 = sb.toCharArray();
int i = chs1.length - 1;
int j = chs2.length - 1;
char[] res = new char[dp[i][j]]; //生成答案的陣列
int index = dp[i][j] - 1;
while(index >= 0){
if(i > 0 && dp[i][j] == dp[i-1][j]){
i--;
}else if(j > 0 && dp[i][j] == dp[i][j-1]){
j--;
}else { // dp[i][j] = dp[i-1][j-1]+1
res[index--] = chs1[i];
i--;
j--;
}
}
return String.valueOf(res);
}
public static void main(String[] args) {
Scanner cin = new Scanner(new BufferedInputStream(System.in));
String sa = cin.next();
String sb = cin.next();
int[][] dp = getDp(sa.toCharArray(), sb.toCharArray());
// System.out.println(dp[sa.length()-1][sb.length()-1]); //length of lcs
System.out.println(getLCS(sa, sb, dp));
}
}
最長公眾子串
題目連結
解析
dp
矩陣第一列即dp[0~N-1][0]
,對某一個位置(i,0)
來說,如果str1[i] == str2[0]
,令dp[i][0] = 1
,否則令dp[i][0] = 0
;- 矩陣
dp
第一行,即dp[0][0~M-1]
,對某個位置(0,j)
來說,如果str1[0] == str2[j]
,令dp[0][j] = 1
,否則令dp[0][j] = 0
; - 一般的位置有兩種情況,如果
str1[i] != str2[j]
,說明在必須把str1[i]
和str2[j]
當做公共子串最後一個字元是不可能的,所以dp[i][j] = 0
; 如果str1[i] = str2[j]
,說明可以將str1[i]
和str2[j]
作為公共子串的最後一個字元,其長度就是dp[i-1][j-1] + 1
;
import java.util.*;
public class LongestSubstring {
public int findLongest(String A, int n, String B, int m) {
char[] sa = A.toCharArray();
char[] sb = B.toCharArray();
int[][] dp = new int[sa.length][sb.length];
for(int i = 0; i < sa.length; i++) //注意和最長公共子序列有點不同
dp[i][0] = sa[i] == sb[0] ? 1 : 0;
for(int j = 0; j < sb.length; j++)
dp[0][j] = sa[0] == sb[j] ? 1 : 0;
int res = 0;
for(int i = 1; i < sa.length; i++){
for(int j = 1; j < sb.length; j++){
if(sa[i] == sb[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
res = Math.max(res, dp[i][j]);
}
}
}
return res; //dp陣列中的最大值,就是最大公共字串的長度
}
}
由dp
表生成答案字串也是不難的,找到最大值,然後往左邊的res
個字元就是答案。
測試程式:
public class LCSub {
public static int[][] getDp(char[] sa,char[] sb){
int[][] dp = new int[sa.length][sb.length];
for(int i = 0; i < sa.length; i++) //注意和最長公共子序列有點不同
dp[i][0] = sa[i] == sb[0] ? 1 : 0;
for(int j = 0; j < sb.length; j++)
dp[0][j] = sa[0] == sb[j] ? 1 : 0;
int res = 0;
for(int i = 1; i < sa.length; i++){
for(int j = 1; j < sb.length; j++){
if(sa[i] == sb[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
res = Math.max(res, dp[i][j]);
}
}
}
System.out.println(res); //4
return dp; //dp陣列中的最大值,就是最大公共字串的長度
}
/** 根據dp表得到答案*/
public static String getLongestSubstring(String sa, String sb, int[][] dp){
if(sa == null || sb == null || sa.length() == 0 || sb.length() == 0)
return "";
int max = 0, end = 0;
for(int i = 0; i < dp.length; i++){
for(int j = 0; j < dp[0].length; j++){
if(dp[i][j] > max){
max = dp[i][j];
end = i;
}
}
}
return sa.substring(end - max + 1, end+1);
}
public static void main(String[] args) {
String sa = "abcdefq";
String sb = "cdefab";
int[][] dp = getDp(sa.toCharArray(), sb.toCharArray());
System.out.println(getLongestSubstring(sa, sb, dp)); // cdef
System.out.println(getLongestSubstring(sa, sb, dp).length()); //4
}
}
另外,還有一種可以優化空間的做法:
- 因為
dp[i][j]
只依賴於左上角位置的dp[i-1][j-1]
,所以用一個變數記錄左上角的值即可。 - 遍歷方向從右上角的斜線開始,一直遍歷到左下角,中間記錄最大值
max
和結束位置end
即可。
程式碼如下:
public static String getLongestSubstring2(String sa,String sb){
if(sa == null || sb == null || sa.length() == 0 || sb.length() == 0)
return "";
char[] chs1 = sa.toCharArray();
char[] chs2 = sb.toCharArray();
int row = 0, col = chs2.length-1; //從右上角開始
int max = 0, end = 0; //記錄最大長度和結束位置
while(row < chs1.length){
int i = row, j = col;
int ul = 0;
while(i < chs1.length && j < chs2.length){ //從(i,j)向右下方開始遍歷
if(chs1[i] == chs2[j])
ul++;
else
ul = 0;
if(ul > max){ //記錄最大值以及結尾字元的位置
max = ul;
end = i;
}
i++;
j++;
}
if(col > 0) // 斜線還沒到最左邊 --> 往左移動
col--;
else
row++; //到了最左 --> 往下移動
}
System.out.println(max);
return sa.substring(end-max+1, end+1); // [end-max+1, end]
}