最大子序列、最長連續公共子串(連續)、最長公共子序列(動態規劃)

原文鏈接:http://blog.sina.com.cn/s/blog_54f82cc20100zi4b.html

最大子序列

最大子序列是要找出由數組成的一維數組中和最大的連續子序列。比如{5,-3,4,2}的最大子序列就是 {5,-3,4,2},它的和是8,達到最大;而 {5,-6,4,2}的最大子序列是{4,2},它的和是6。你已經看出來了,找最大子序列的方法很簡單,只要前i項的和還沒有小於0那麼子序列就一直向後擴展,否則丟棄之前的子序列開始新的子序列,同時我們要記下各個子序列的和,最後找到和最大的子序列。

代碼如下:

#include<iostream>
using namespace std;
int MaxSubSeq(const int *arr,int len,int *start,int *end){
    int max=0;                  //記錄目前找到的最大子序列的和
    int sum=0;                  //記錄當前子序列的和
    int begin=0,finish=0;       //記錄當前子序列的起始下標
    *start=begin;*end=finish;   //記錄最長子序列的起始下標
    for(int i=0;i<len;i++){
        sum+=arr[i];
        finish=i;
        if(sum>max){
            max=sum;
            *end=finish;
            *start=begin;
        }
        if(sum<=0){
            sum=0;
            begin=i+1;
        }
    }
    return max;
}
int main(){
    int arr[6]={5,-3,-2,12,9,-1};
    int start,end;
    int max=MaxSubSeq(arr,6,&start,&end);
    cout<<"The MaxSubSeq is from position "<<start<<"to position "<<end<<"."<<endl;
    cout<<"Sum of MaSubSeq: "<<max<<endl;
    return 0;
}

最長公共子串(LCS)

找 兩個字符串的最長公共子串,這個子串要求在原字符串中是連續的。其實這又是一個序貫決策問題,可以用動態規劃來求解。我們採用一個二維矩陣來記錄中間的結 果。這個二維矩陣怎麼構造呢?直接舉個例子吧:"bab"和"caba"(當然我們現在一眼就可以看出來最長公共子串是"ba"或"ab")

   b  a  b

c  0  0  0

a  0  1  0

b  1  0  1

a  0  1  0

我們看矩陣的斜對角線最長的那個就能找出最長公共子串。

不過在二維矩陣上找最長的由1組成的斜對角線也是件麻煩費時的事,下面改進:當要在矩陣是填1時讓它等於其左上角元素加1。

   b  a  b

c  0  0  0

a  0  1  0

b  1  0  2

a  0  2  0

這樣矩陣中的最大元素就是 最長公共子串的長度。

在構造這個二維矩陣的過程中由於得出矩陣的某一行後其上一行就沒用了,所以實際上在程序中可以用一維數組來代替這個矩陣。

代碼如下:

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
//str1爲橫向,str2這縱向
const string LCS(const string& str1,const string& str2){
    int xlen=str1.size();       //橫向長度
    vector<int> tmp(xlen);        //保存矩陣的上一行
    vector<int> arr(tmp);     //當前行
    int ylen=str2.size();       //縱向長度
    int maxele=0;               //矩陣元素中的最大值
    int pos=0;                  //矩陣元素最大值出現在第幾列
    for(int i=0;i<ylen;i++){
        string s=str2.substr(i,1);
        arr.assign(xlen,0);     //數組清0
        for(int j=0;j<xlen;j++){
            if(str1.compare(j,1,s)==0){
                if(j==0)
                    arr[j]=1;
                else
                    arr[j]=tmp[j-1]+1;
                if(arr[j]>maxele){
                    maxele=arr[j];
                    pos=j;
                }
            }      
        }
//      {
//          vector<int>::iterator iter=arr.begin();
//          while(iter!=arr.end())
//              cout<<*iter++;
//          cout<<endl;
//      }
        tmp.assign(arr.begin(),arr.end());
    }
    string res=str1.substr(pos-maxele+1,maxele);
    return res;
}
int main(){
    string str1("21232523311324");
    string str2("312123223445");
    string lcs=LCS(str1,str2);
    cout<<lcs<<endl;
    return 0;
}


最長公共子序列

最長公共子序列與最長公共子串的區別在於最長公共子序列不要求在原字符串中是連續的,比如ADE和ABCDE的最長公共子序列是ADE。

我們用動態規劃的方法來思考這個問題如是求解。首先要找到狀態轉移方程:

等號約定,C1是S1的最右側字符,C2是S2的最右側字符,S1‘是從S1中去除C1的部分,S2'是從S2中去除C2的部分。

LCS(S1,S2)等於下列3項的最大者:

(1)LCS(S1,S2’)

(2)LCS(S1’,S2)

(3)LCS(S1’,S2’)--如果C1不等於C2; LCS(S1',S2')+C1--如果C1等於C2;

邊界終止條件:如果S1和S2都是空串,則結果也是空串。

下面我們同樣要構建一個矩陣來存儲動態規劃過程中子問題的解。這個矩陣中的每個數字代表了該行和該列之前的LCS的長度。與上面剛剛分析出的狀態轉移議程相對應,矩陣中每個格子裏的數字應該這麼填,它等於以下3項的最大值:

(1)上面一個格子裏的數字

(2)左邊一個格子裏的數字

(3)左上角那個格子裏的數字(如果 C1不等於C2); 左上角那個格子裏的數字+1( 如果C1等於C2)

舉個例子:

       G  C  T  A

   0  0  0  0  0

G  0  1  1  1  1

B  0  1  1  1  1

T  0  1  1  2  2

A    0  1  1  2  3

填寫最後一個數字時,它應該是下面三個的最大者:

(1)上邊的數字2

(2)左邊的數字2

(3)左上角的數字2+1=3,因爲此時C1==C2

所以最終結果是3。

在填寫過程中我們還是記錄下當前單元格的數字來自於哪個單元格,以方便最後我們回溯找出最長公共子串。有時候左上、左、上三者中有多個同時達到最大,那麼任取其中之一,但是在整個過程中你必須遵循固定的優先標準。在我的代碼中優先級別是左上>左>上。

奉上代碼:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<iostream>
#include<cstring>
#include<stack>
#include<utility>
#define LEFTUP 0
#define LEFT 1
#define UP 2
using namespace std;
int Max(int a,int b,int c,int *max){            //找最大者時a的優先級別最高,c的最低.最大值保存在*max中
    int res=0;          //res記錄來自於哪個單元格
    *max=a;
    if(b>*max){
        *max=b;
        res=1;
    }
    if(c>*max){
        *max=c;
        res=2;
    }
    return res;
}
//調用此函數時請注意把較長的字符串賦給str1,這主要是爲了在回溯最長子序列時節省時間。如果沒有把較長的字符串賦給str1不影響程序的正確執行。
string LCS(const string &str1,const string &str2){
    int xlen=str1.size();               //橫向長度
    int ylen=str2.size();               //縱向長度
    if(xlen==0||ylen==0)                //str1和str2中只要有一個爲空,則返回空
        return "";
    pair<int,int> arr[ylen+1][xlen+1];    //構造pair二維數組,first記錄數據,second記錄來源
    for(int i=0;i<=xlen;i++)         //首行清0
        arr[0][i].first=0;
    for(int j=0;j<=ylen;j++)         //首列清0
        arr[j][0].first=0;
    for(int i=1;i<=ylen;i++){
        char s=str2.at(i-1);
        for(int j=1;j<=xlen;j++){
            int leftup=arr[i-1][j-1].first;
            int left=arr[i][j-1].first;
            int up=arr[i-1][j].first;
            if(str1.at(j-1)==s)         //C1==C2
                leftup++;
            int max;
            arr[i][j].second=Max(leftup,left,up,&arr[i][j].first);
//          cout<<arr[i][j].first<<arr[i][j].second<<" ";
        }
//      cout<<endl;
    }      
    //回溯找出最長公共子序列
    stack<int> st;
    int i=ylen,j=xlen;
    while(!(i==0&&j==0)){
        if(arr[i][j].second==LEFTUP){
            if(arr[i][j].first==arr[i-1][j-1].first+1)
                st.push(i);
            --i;
            --j;
        }
        else if(arr[i][j].second==LEFT){
            --j;
        }
        else if(arr[i][j].second==UP){
            --i;
        }
    }
    string res="";
    while(!st.empty()){
        int index=st.top()-1;
        res.append(str2.substr(index,1));
        st.pop();
    }
    return res;
}
int main(){
    string str1="GCCCTAGCG";
    string str2="GCGCAATG";
    string lcs=LCS(str1,str2);
    cout<<lcs<<endl;
    return 0;
}

下面給一個Java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static <E> List<E> longestCommonSubsequence(E[] s1,E[] s2){
        int[][] num=new int[s1.length+1][s2.length+1];
        for(int i=1;i<s1.length;i++){
            for(int j=1;j<s2.length;j++){
                if(s1[i-1].equals(s2[j-1])){
                    num[i][j]=1+num[i-1][j-1];
                }
                else{
                    num[i][j]=Math.max(num[i-1][j],num[i][j-1]);
                }
            }
        }
        System.out.println("lenght of LCS= "+num[s1.length][s2.length]);
        int s1position=s1.length,s2position=s2.length;
        List<E> result=new LinkedList<E>();
        while(s1position!=0 && s2position!=0){
            if(s1[s1position-1].equals(s2[s2position-1])){
                result.add(s1[s1position-1]);
                s1position--;
                s2position--;
            }
            else if(num[s1position][s2position-1]>=num[s1position-1][s2position]){
                s2position--;
            }
            else{
                s1position--;
            }
        }
        Collections.reverse(result);
        return result;
    }

std::endl是一個特殊值,稱爲操縱符(manipulator),將它寫入輸出流時具有輸出換行的效果,並刷新與設備相關聯的緩衝區(buffer)。通過刷新緩衝區用戶可立即看到寫入到流中的輸出。

我在調試以上代碼時在45行(cout<<endl;)處設置斷點,結果發現“43行(cout<<arr[i][j].first<<arr[i][j].second<<"";) 沒有執行”,這就是因爲43行末尾沒有加endl,所以用戶沒有立即看到輸出流中的數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章