JAVA動態規劃(二)--最長公共子序列問題(LCS_subSequence)的三種解法與最長公共子字串(LCS_subString)的兩種解法與最長迴文串(LongestPalindrome)
動態規劃法
經常會遇到複雜問題不能簡單地分解成幾個子問題,而會分解出一系列的子問題。簡單地採用把大問題分解成子問題,並綜合子問題的解匯出大問題的解的方法,問題求解耗時會按問題規模呈冪級數增加。
為了節約重複求相同子問題的時間,引入一個數組,不管它們是否對最終解有用,把所有子問題的解存於該陣列中,這就是動態規劃法所採用的基本方法。
本文將介紹最長公共子序列問題(LCS_subSequence)的三種解法與最長公共子字串(LCS_subString)問題的兩種解法。
一、最長公共子子序列問題(LCS_subSequence)的三種解法
【問題】 求兩字元序列的最長公共字元子序列
問題描述:字元序列的子序列是指從給定字元序列中隨意地(不一定連續)去掉若干個字元(可能一個也不去掉)後所形成的字元序列。令給定的字元序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一個嚴格遞增下標序列[i0,i1,…,ik-1],使得對所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一個子序列。
考慮最長公共子序列問題如何分解成子問題,設A=“a0,a1,…,am-1”,B=“b0,b1,…,bm-1”,並Z=“z0,z1,…,zk-1”為它們的最長公共子序列。不難證明有以下性質:
(1) 如果am-1=bn-1,則zk-1=am-1=bn-1,且“z0,z1,…,zk-2”是“a0,a1,…,am-2”和“b0,b1,…,bn-2”的一個最長公共子序列;
(2) 如果am-1!=bn-1,則若zk-1!=am-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列;
(3) 如果am-1!=bn-1,則若zk-1!=bn-1,蘊涵“z0,z1,…,zk-1”是“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列。
這樣,在找A和B的公共子序列時,如有am-1=bn-1,則進一步解決一個子問題,找“a0,a1,…,am-2”和“b0,b1,…,bm-2”的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出“a0,a1,…,am-2”和“b0,b1,…,bn-1”的一個最長公共子序列和找出“a0,a1,…,am-1”和“b0,b1,…,bn-2”的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。
求解:
引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。
我們是自底向上進行遞推計算,那麼在計算c[i,j]之前,c[i-1][j-1],c[i-1][j]與c[i][j-1]均已計算出來。此時我們根據X[i] = Y[j]還是X[i] != Y[j],就可以計算出c[i][j]。
問題的遞迴式寫成:
演算法分析:由於每次呼叫至少向上或向左(或向上向左同時)移動一步,故最多呼叫(m * n)次就會遇到i = 0或j = 0的情況,此時開始返回。返回時與遞迴呼叫時方向相反,步數相同,故演算法時間複雜度為Θ(m * n)。
Java程式碼:
/*
* 最長公共子序列問題
* 方法(一):遞迴:返回最長子序列長度
* */
public class LCS_problem {
public static void main(String[] args) {
String s1="abcdefghijkl";
String s2="cdafu";
int l=new LCS_problem().lcs(s1, s1.length()-1, s2, s2.length()-1);
System.out.println(l);
}
public int lcs(String s1,int x,String s2,int y){
if(x<0||y<0){
return 0;
}
if(s1.charAt(x)==s2.charAt(y)){
return lcs(s1,x-1,s2,y-1)+1; //如果末位相同,則子序列長度加1
}else{ //否則比較兩個字串分別去掉一個字元的子序列長度
int l1=lcs(s1,x-1,s2,y);
int l2=lcs(s1,x,s2,y-1);
return l1>l2?l1:l2;
}
}
}
遞迴只能輸出最長子序列的長度,不能輸出具體的子序列,下面我們看看如何用動態規劃的方法輸出最長公共子序列:
/*
* 最長公共子序列問題
* 動態規劃【方法一:對開始字元初始狀態單獨處理】:返回最長子序列長度int 以及具體的子序列String
* */
public class LCS_problem {
public static void main(String[] args) {
String s1="abcdefghijkl";
String s2="1b2ck3";
char []x=s1.toCharArray();
char []y=s2.toCharArray();
int b[][]=new LCS_problem().getLength(x,y);
new LCS_problem().subSequence(b,x,x.length-1,y.length-1);
}
public int[][] getLength(char[] x,char[] y){
int l1=x.length;
int l2=y.length;
/*引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,
* b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。*/
int[][]c=new int[l1][l2];
int[][]b=new int[l1][l2];
for(int i=0;i<l1;i++){
for(int j=0;j<l2;j++){
if(i==0||j==0){ //處理i=0或者j=0的時候,陣列下標不能-1的問題,並且此時c[i][j]最大為1
if(x[i]==y[j]){
c[i][j]=1;
b[i][j]=1;
}
else if(j>0){
c[i][j]=c[i][j-1];
b[i][j]=-1;
}
else if(i>0){
c[i][j]=c[i-1][j];
b[i][j]=0;
}
}
else if(x[i]==y[j]){
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if(c[i-1][j]>=c[i][j-1]){
c[i][j]=c[i-1][j];
b[i][j]=0;
}
else{
c[i][j]=c[i][j-1];
b[i][j]=-1;
}
}
}
System.out.println("The length of sublsequence="+c[l1-1][l2-1]);
return b;
}
public void subSequence(int[][]b,char[]x,int m,int n){//m,n分別為s1,s2的長度
if(m<0||n<0){
return;
}
if(b[m][n]==1){
subSequence(b,x,m-1,n-1);
System.out.print(x[m]+"\t");
}
else if(b[m][n]==0){
subSequence(b,x,m-1,n);
}
else{
subSequence(b,x,m,n-1);
}
}
當然,這裡的程式碼重複度有點高,因為對字元的初始狀態做了單獨的處理,我們還可以通過對字串做處理,減少程式碼重複度。
/*
* 最長公共子序列問題
* 動態規劃:返回最長子序列長度int 以及具體的子序列String
【方法二:字串前面加空格,使第一個字元和後面的字元可以統一處理】
* */
public class LCS_problem {
public static void main(String[] args) {
String s1="abcdefghijkl";
String s2="1b2ck3";
//前面加空格的原因是為了把字串第一個字元和後面的字元統一處理,不用單獨處理
char []x=(" "+s1).toCharArray();
char []y=(" "+s2).toCharArray();
int b[][]=new LCS_problem().getLength(x,y);
new LCS_problem().subSequence(b,x,x.length-1,y.length-1);
}
public int[][] getLength(char[] x,char[] y){
int l1=x.length;
int l2=y.length;
/*引進一個二維陣列c[][],用c[i][j]記錄X[i]與Y[j] 的LCS 的長度,
* b[i][j]記錄c[i][j]是通過哪一個子問題的值求得的,以決定搜尋的方向。*/
int[][]c=new int[l1][l2];
int[][]b=new int[l1][l2];
for(int i=1;i<l1;i++){
for(int j=1;j<l2;j++){
if(x[i]==y[j]){
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if(c[i-1][j]>=c[i][j-1]){
c[i][j]=c[i-1][j];
b[i][j]=0;
}
else{
c[i][j]=c[i][j-1];
b[i][j]=-1;
}
}
}
System.out.println("The length of sublsequence="+c[l1-1][l2-1]);
return b;
}
public void subSequence(int[][]b,char[]x,int m,int n){//m,n分別為s1,s2的長度
if(m<0||n<0){
return;
}
if(b[m][n]==1){
subSequence(b,x,m-1,n-1);
System.out.print(x[m]+"\t");
}
else if(b[m][n]==0){
subSequence(b,x,m-1,n);
}
else{
subSequence(b,x,m,n-1);
}
}
二、最長公共子字串(LCS_subString)問題的兩種解法
package dynamic_programming;
/**
* @author Gavenyeah
*
* @date Time: 2016年4月3日下午12:45:59
*/
//查詢最長公共子字串的長度和輸出子字串
public class LCS_subString {
public static void main(String[] args) {
String s1="1234abc22";
String s2="1212345abc22";
char[]x=(" "+s1).toCharArray();
char[]y=(" "+s2).toCharArray();
int[][]c=new LCS_subString().getLength(x, y);
System.out.print("方法一-->動態規劃求解:");
new LCS_subString().lcs_subString(c,x,x.length,y.length);
System.out.println();
System.out.println("**********************************************");
System.out.print("方法二-->陣列比較求解:");
new LCS_subString().lcs_subString(x,y);
}
//方法一-->動態規劃求解
/*動態轉移方程為:
如果xi == yj, 則 c[i][j] = c[i-1][j-1]+1
如果xi ! = yj, 那麼c[i][j] = 0*/
public void lcs_subString(int[][]c,char[] x,int m,int n){
int max=0;
int p=0;
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(max<c[i][j]){
max=c[i][j];
p=i;
}
}
}
System.out.println("The length of LCS_subString:"+max);
while(max>0){
max--;
System.out.print(x[p-max]);
}
}
public int[][]getLength(char[]x,char[]y){
int[][]c=new int[x.length][y.length];
for(int i=1;i<x.length;i++){
for(int j=1;j<y.length;j++){
if(x[i]==y[j]){
c[i][j]=c[i-1][j-1]+1;
}
else{
c[i][j]=0;
}
}
}
return c;
}
//方法二-->陣列比較求解
public void lcs_subString(char x[],char y[]){
int len=0;//記錄最長子字串的長度
int loc=0;//記錄最長子字串開始的位置
for(int i=0;i<x.length;i++){
for(int j=0;j<y.length;j++){
int count=0;//記錄當前比較的子字串的長度
int m=i,n=j;
while(m<x.length&&n<y.length&&x[m]==y[n]){
count++;
m++;
n++;
}
if(len<count){
len=count;
loc=i;
}
if(count>0) //加快查詢速度
j=j+count-1;
}
}
System.out.println("The length of LCS_subString:"+len);
while(len>0){
System.out.print(x[loc]);
len--;
loc++;
}
}
}
3.最長迴文字串的兩種解法
最長迴文字串即類似:aba,abba等這些形式的。
package dynamic_programming;
/**
* @author Gavenyeah
*
* @date Time: 2016年4月3日下午5:53:58
*/
public class LongestPalindrome {
public static void main(String[] args) {
String s="123321abccba12332";
// new LongestPalindrome().palindrome(s);
new LongestPalindrome().getPalindrome(s);
}
//將可能是迴文串的字元取出進行比較
public void getPalindrome(String s){
String longestPa=s.substring(0,1);//附初值,以及當s的長度為1的時候的返回值
int maxLen=0;
for(int i=0;i<s.length();i++){
for(int j=i+1;j<s.length();j++){
if(s.charAt(i)==s.charAt(j)){
String pa=s.substring(i,j+1);
int len=j-i+1;
if(maxLen<len&&isPalindrome(pa,len)){
maxLen=len;
longestPa=pa;
}
}
}
}
System.out.println(longestPa);
}
public boolean isPalindrome(String pa,int len){
for(int i=0,j=len-1;i<=j;i++,j--){
if(pa.charAt(i)!=pa.charAt(j)){
return false;
}
}
return true;
}
/*
//逐步向後查詢法
public void palindrome(String s){
if(s.length()==1||(s.length()==2&&s.charAt(0)==s.charAt(1))){
System.out.print(s);
return;
}
char c[]=s.toCharArray();
int max=0;
int loc=0;
for(int i=2;i<c.length;i++){
int count=0;
int k=i;
int j=i-1;
if(c[k]==c[j]){
while((k<c.length&&j>=0)&&c[k]==c[j]){
count++;
k++;
j--;
}
count*=2;
}
else if(c[k]==c[j-1]){
while((k<c.length&&j>=0)&&c[k]==c[j-1]){
count++;
k++;
j--;
}
count=count*2+1;
}
if(max<count){
max=count;
if(max%2==0){
loc=j+1;
}
else loc=j;
}
}
int k=1;
while(k<=max){
System.out.print(c[loc]);
loc++;
k++;
}
}*/
}