最长公共子序列(LCS问题)
先簡(jiǎn)單介紹下什么是最長(zhǎng)公共子序列問題,其實(shí)問題很直白,假設(shè)兩個(gè)序列X,Y,X的值是ACBDDCB,Y的值是BBDC,那么XY的最長(zhǎng)公共子序列就是BDC。這里解決的問題就是需要一種算法可以快速的計(jì)算出這個(gè)最大的子序列,當(dāng)然,用最簡(jiǎn)單的方法就是列出XY全部的子系列然后一個(gè)個(gè)對(duì)比,但這樣的時(shí)間復(fù)雜度是絕對(duì)不能接受的。假設(shè)X的長(zhǎng)度是m,Y的長(zhǎng)度是n,拿X的一個(gè)子序列和Y進(jìn)行對(duì)比的時(shí)間是n,計(jì)算X的全部子序列的時(shí)間是2^m,所以,如果采用的是一個(gè)個(gè)全部計(jì)算的話,將會(huì)花費(fèi)n*2^m的時(shí)間,指數(shù)級(jí)別的時(shí)間復(fù)雜度是爆炸式的。我們這里解決的方法是采用動(dòng)態(tài)規(guī)劃的方式,所以再講問題之前,先簡(jiǎn)單提下動(dòng)態(tài)規(guī)劃的概念。
動(dòng)態(tài)規(guī)劃
動(dòng)態(tài)規(guī)劃是通過組合子問題的解而解決整個(gè)問題的,說到這里,是不是有些熟悉?在先前的歸并排序中采用的分治法其實(shí)也是對(duì)子問題進(jìn)行分析,但有所不同的是,分治法的思想是通過將問題分解為多個(gè)子問題,然后一一解決,最后合并子問題就得到了原問題的答案。當(dāng)然,分治法所適用的領(lǐng)域就是子問題沒有相互的關(guān)聯(lián),而動(dòng)態(tài)規(guī)劃所就沒那么簡(jiǎn)單了,它的子問題一般都是由相互關(guān)聯(lián)的情況,也就是說子問題包含了公共的子子問題。什么叫公共的子子問題,就是一個(gè)子問題繼續(xù)分解,另一個(gè)子問題也繼續(xù)分解,然后它們驚訝的發(fā)現(xiàn)它們分解出來的問題竟然是一樣的。所以假設(shè)使用分治法來計(jì)算這種問題的話,就會(huì)產(chǎn)生許多不必要的重復(fù)計(jì)算,而動(dòng)態(tài)規(guī)劃的目標(biāo)之一就是去除這種重復(fù)的計(jì)算,而方法就是講結(jié)果放在一張表中,具體的怎么弄可以詳細(xì)看最長(zhǎng)公共子序列問題怎么解的。
動(dòng)態(tài)規(guī)劃常常用于最優(yōu)解問題,具體的設(shè)計(jì)可以參考下面的部分:
1.描述最優(yōu)解的結(jié)構(gòu)
2.遞歸定義最優(yōu)解的值
3.按自底向上的方式計(jì)算最優(yōu)解的值
4.由計(jì)算結(jié)果構(gòu)造一個(gè)最優(yōu)解
這幾個(gè)步驟等等就會(huì)被用于求解最長(zhǎng)公共子序列的問題上,具體步驟請(qǐng)對(duì)號(hào)入座。
最長(zhǎng)公共子序列可以用來干嘛?
在解決這個(gè)問題之前,我們當(dāng)然需要?jiǎng)偳宄约核愠鰜淼臇|西有什么用處吧~就直接說書上的例子吧,生物里面的DNA由ACGT四種堿基構(gòu)成,兩種生物的DNA序列就是這四種堿基的集合,而有時(shí)候就是需要檢測(cè)兩種生物DNA的近似度,一般采用的方法比較多,可以檢測(cè)一種生物的DNA是不是另一種生物DNA的子集,或者另一種方式,就是如果有第三條DNA序列同時(shí)出現(xiàn)在前面兩條DNA之中,DNA出現(xiàn)的序列順序必須相同,但并不是一定要連續(xù)出現(xiàn)。所以,尋找到的那個(gè)公共的DNA序列越長(zhǎng),那么兩個(gè)生物DNA就越近似。
應(yīng)用先將這么多,首先需要詳細(xì)定義最大公共子序列問題。
一個(gè)給定序列的子序列就是該給定序列中去掉零個(gè)或者多個(gè)元素。另一種形式化的定義(看不看無所謂,看懂開篇說明的話就可以略過),給定一個(gè)序列X=<x1,x2,...,xm>,另一個(gè)序列Z=<z1,z2,...,zk>是X的一個(gè)子序列,如果存在X的一個(gè)嚴(yán)格遞增下標(biāo)序列<i1,i2,...,ik>,使得對(duì)所有的j=1,2,...,k有xij=zj(注意這里左邊j是i的下標(biāo))。
給定兩個(gè)序列X和Y,Z是X和Y的公共子序列,假若Z的長(zhǎng)度是所有的X和Y的公共子序列中最長(zhǎng)的,那么稱Z為最長(zhǎng)公共子序列,算法的目的就是為了求解這些最長(zhǎng)公共子序列(注意,Z并不一定唯一)
步驟一:描述一個(gè)最長(zhǎng)公共子序列
對(duì)于以下的最長(zhǎng)公共子序列問題,簡(jiǎn)稱為L(zhǎng)CS問題。之前提到的將XY中所有的子序列全部計(jì)算出來再進(jìn)行比較顯然是不現(xiàn)實(shí)的,但是,對(duì)于LCS問題而言,具有最優(yōu)子結(jié)構(gòu)特征,具體的說明在下面的定理中。這里,我們先給出前綴的概念,具體定義就不提了,就舉一個(gè)例子,假設(shè)X=<A,B,C,B,D,A,B>,那么X4=<A,B,C,B>就是X的一個(gè)前綴,顯然,X0為空。而下面分解的子問題其實(shí)就是分解為前綴。
定理(LCS的最優(yōu)子結(jié)構(gòu)):
設(shè)X=<x1,x2,...,xm>,Y=<y1,y2,...,yn>為兩個(gè)序列,并且Z=<z1,z2,...,zk>為X和Y的任意一個(gè)LCS
1.如果xm=yn,那么zk=xm=yn而且Z(k-1)是X(m-1)和Y(n-1)的一個(gè)LCS
2.如果xm!=yn,那么zk!=xm蘊(yùn)含Z是X(m-1)和Y的一個(gè)LCS
3.如果xm!=yn,那么zk!=yn蘊(yùn)含Z是X和Y(n-1)的一個(gè)LCS
這些性質(zhì)的作用其實(shí)就是為了說明兩個(gè)序列的LCS包含了兩個(gè)序列前綴的LCS,那么LCS就具有最優(yōu)子結(jié)構(gòu)性質(zhì)(最優(yōu)子結(jié)構(gòu)就是一個(gè)最優(yōu)解包含了子結(jié)構(gòu)的最優(yōu)解)。
步驟二:一個(gè)遞歸解
從上面的定理我們可以知道,在尋找LCS的時(shí)候,一般就是從子序列中尋找LCS,這樣就要檢查一個(gè)或者兩個(gè)字問題。如果xm=yn,那么就要找出X(m-1)和Y(n-1)的LCS,然后將xm=yn加上去,就是X和Y的LCS了。但如果xm!=yn的話,那么就要分別計(jì)算X(m-1)和Y以及X和Y(n-1)的兩個(gè)LCS,比價(jià)長(zhǎng)的LCS就是要找的。這樣雖然表面上是遞歸分治的問題,但實(shí)際上如果仔細(xì)觀察的話就會(huì)發(fā)現(xiàn)LCS問題中的重疊子問題,比如對(duì)于X(m-1)和Y的子問題,和X和Y(n-1)的子問題中,同時(shí)包含了X(m-1)和Y(n-1)的子子問題,這樣就導(dǎo)致了重復(fù)計(jì)算的問題,這樣使用分治直接解決就不適用了。具體的解決方法后文將會(huì)描述。
要求出最長(zhǎng)的子序列,首先要知道的是最長(zhǎng)有多長(zhǎng)。這里定義C[i,j]為序列Xi和Yj的一個(gè)LCS的長(zhǎng)度,可得到遞歸式:
步驟三:計(jì)算LCS的長(zhǎng)度
其實(shí)就是根據(jù)上面的遞歸式給出程序,這里先給出偽代碼,具體程序最后給出:
可以看到,在程序中有兩個(gè)二維數(shù)組c和b,c用來記錄最長(zhǎng)子序列的長(zhǎng)度,b的話相當(dāng)于記錄了軌跡,在最后構(gòu)造LCS的時(shí)候起到作用。
下圖展示的就是程序運(yùn)行后表格c和b的具體情形(畫在了一張表里)
步驟四:構(gòu)造一個(gè)LCS
只要得到了表b,就可以快速構(gòu)造出LCS,偽代碼如下:
這是一個(gè)遞歸算法,一開始i和j的值為m和n,就是從表格的右下角開始回溯,從而得到LCS。
下面給出具體的C語(yǔ)言實(shí)現(xiàn)過程:
?
#include <stdio.h> #include <stdlib.h>#define m 7 #define n 6int c[m+1][n+1]; char b[m+1][n+1];void LCS_LENGTH(char* X,char* Y) {int i,j;for(i=1;i<=m;i++)c[i][0]=0;for(j=0;j<=n;j++)c[0][j]=0;for(i=1;i<=m;i++){for(j=1;j<=n;j++){if(X[i]==Y[j]){c[i][j]=c[i-1][j-1]+1;b[i][j]='\\';}else{if(c[i-1][j]>=c[i][j-1]){c[i][j]=c[i-1][j];b[i][j]='|';}else{c[i][j]=c[i][j-1];b[i][j]='-';}}}} }void PRINT_LCS(char* X,int i,int j) {if(i==0 || j==0)return;if(b[i][j]=='\\'){PRINT_LCS(X,i-1,j-1);printf("%c ",X[i]);}else if(b[i][j]=='|')PRINT_LCS(X,i-1,j);elsePRINT_LCS(X,i,j-1); }int main(void) {int i,j;char X[m+1]={'X','A','B','C','B','D','A','B'};char Y[n+1]={'Y','B','D','C','A','B','A'};LCS_LENGTH(X,Y);printf("LCS長(zhǎng)度表c打印出來是這個(gè)樣子:\n");for(i=1;i<=m;i++){for(j=1;j<=n;j++)printf("%d ",c[i][j]);printf("\n");}printf("路徑表b打印出來是這個(gè)樣子\n");for(i=1;i<=m;i++){for(j=1;j<=n;j++)printf("%c ",b[i][j]);printf("\n");}printf("\nLCS的具體值是:\n");PRINT_LCS(X,m,n);return 0; }?
當(dāng)然了,改進(jìn)代碼的方式也有很多,對(duì)空間的改進(jìn)可以完全不使用表b等等,這里就不再詳細(xì)敘述了。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的最长公共子序列(LCS问题)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: InnoDB和MyISAM的区别与选择
- 下一篇: rails3和4获取当前url