1. 程式人生 > >動態規劃之LCS和LIS

動態規劃之LCS和LIS

(一)最長公共子序列

       問題描述:給定一個序列 , 序列  稱為 X 的子序列,當存在一個嚴格遞增的X的下標序列  ,對所有的 ,都有 。求兩個序列的長度最長的公共子序列的問題就被稱為最長公共子序列問題(Longest-common-subsequence problem, LCS)。        注意,在這個問題中,子序列中的元素不一定在原序列中是相鄰的。        這是經典的動態規劃問題,具體的分析過程,許多演算法書上都講過了,可以參考《演算法導論》第十五章。下面兩張圖片取自《演算法導論》第十五章,描述的是該問題的轉移方程及及動態生成最優解的過程。

        下面是針對
HDU 1159
 問題的我的AC程式碼:
//hdj1159
//dynamic programing, LCS

#include <iostream>
#include <string>

using namespace std;

int LCS(string s1, string s2)
{
   int len1 = s1.length();
   int len2 = s2.length();
   int** a = new int*[len1 + 1];
   int i, j;   
   for(i = 0 ; i < len1+1 ; i++) a[i] = new int[len2+1];
   for(i = 0 ; i < len1+1 ; i++) a[i][0] = 0;
   for(j = 0 ; j < len2+1 ; j++) a[0][j] = 0;
   for(i = 1 ; i < len1+1 ; i++)
   {
       for(j = 1 ; j < len2+1; j++)
       {
           if(s1[i-1] == s2[j-1]) a[i][j] = a[i-1][j-1]+1;
           else if( a[i-1][j] > a[i][j-1] ) a[i][j] = a[i-1][j];
           else a[i][j] = a[i][j-1];                   
       }               
   } 
   return a[len1][len2];
}
int main()
{
    string s1, s2;
    while(cin>>s1>>s2)
    {
       cout<<LCS(s1, s2)<<endl;                  
    }    
    return 0;   
}


(二)帶權值的最長公共子序列

       問題描述,HDU 1080 , 給定兩個由指定字母組成的字串,可以在兩個字串的任意位置新增任意的字元 ‘-’ ,現在要求將兩個字串進行配對,同時,任意兩個字元之間配對都有一個權值,要求使得總權值最大的匹配。

       假設原字串是 , 設  表示  的字首子串  與  的字首子串  之間的最大匹配值。則  的生成有三種情況:

       (1)在  的位置插入字元 ‘-’ 。此時, 

       (2)在   的位置插入字元 ‘-’ 。此時, 

       (3)直接令  與  進行匹配。 此時, 

當然, 取三者中的最大者,因此有以下轉移方程:


根據轉移方程,不難給出該問題的解答。以下是我的AC的程式碼:

//hdj1180
//dynamic programing, LCS with weight 
//
#include <iostream>
#include <string>

#define Inf 999999
using namespace std;

int weight[5][5]={
    5, -1, -2, -1, -3,
    -1, 5, -3, -2, -4,
    -2, -3, 5, -2, -2,
    -1, -2, -2, 5, -1,
    -3, -4, -2, -1, -Inf
};
int findindex(char ch)
{
    if(ch == 'A') return 0;
    else if(ch == 'C') return 1;
    else if(ch == 'G') return 2;
    else return 3;
}
int max(int a, int b, int c)
{
   return a>b?a>c?a:c:b>c?b:c;   
}
int LCS(string s1, int len1, string s2, int len2)
{
   int** a = new int*[len1 + 1];
   int i, j;   
   for(i = 0 ; i < len1+1 ; i++) a[i] = new int[len2+1];
   a[0][0] = 0;
   for(i = 1 ; i < len1+1 ; i++) a[i][0] = a[i-1][0] + weight[4][findindex(s1[i-1])];
   for(j = 1 ; j < len2+1 ; j++) a[0][j] = a[0][j-1] + weight[findindex(s2[j-1])][4];
   for(i = 1 ; i < len1+1 ; i++)
   {
       for(j = 1 ; j < len2+1; j++)
       {
            int tmp1 = a[i-1][j-1] +weight[findindex(s1[i-1])][findindex(s2[j-1])];
            int up = a[i-1][j] + weight[findindex(s1[i-1])][4];
            int left = a[i][j-1] + weight[4][findindex(s2[j-1])];
            a[i][j] = max(tmp1, up, left);                   
       }               
   } 
   return a[len1][len2];
}
int main()
{
    
    int T;
    cin>>T;
    while(T--)
    {
       string s1, s2;
       int len1, len2;
       cin>>len1>>s1;
       cin>>len2>>s2;
       cout<<LCS(s1, len1, s2,len2)<<endl;                    
    }  
    return 0;   
}

(三)最長遞增子序列

       問題描述,求一個給定序列中最長的遞增子序列的長度。

方法1:排序+LCS

        一個簡單的思路是將給定的序列先進行有序化(O(nlog(n))),然後使用LCS演算法來查詢給定的序列及有序化後的序列之間的最長公共子串(O(n2))。這個方法顯然不夠好。

方法2:DP

        為了有效的使用DP演算法,我們先要描述該問題的最優子結構性質。設  表示 A的字首子序列  的最長遞增子序列的長度。現在考慮  的值。對於  的任意一個長度為的遞增子序列,如果  能夠新增到該序列的末尾構成一個新的遞增子序列,那麼 。顯然,列舉所有這樣的,將得到不同的, 取其最大者則得 最終的。即有如下轉移方程:

顯然這是可以用動態規劃從左往右計算的。演算法的實現程式碼如下;
int a[MAXN];

int LIS_DP(int n)
{
    int * lenth = new int[n];
    int i;
    for(i=0;i<n;i++)
    {
        lenth[i] = 1;
        for(int j=0;j<i;j++)
           if( a[j] < a[i] && lenth[j]+1 > lenth[i]) lenth[i] = lenth[j]+1;                                             
    }   
    int max = lenth[0];
    for(i = 1 ; i < n ; i++)
      if(lenth[i] > max) max = lenth[i];
    delete [] lenth;
    return max;
}

這個演算法的時間複雜度是 。主要的時間消耗在內層 for 迴圈上,而記憶體for迴圈的作用其實是可以進行優化的。這就是下面的演算法。

方法3:更有效的DP演算法

       觀察原轉移方程  , 可以發現,  儲存的是 的最長遞增子序列的長度,而  的最長遞增子序列不一定是以  結尾的。但,我們在計算  的時候卻依然要將 每一個   與 進行比較,因為我們並不知道長度為  的遞增子序列的結尾元素最小是多少,因此也就不能判斷是否能將 新增到長度為  的遞增子序列的末尾。這樣做是很浪費時間的,因為每次要確定  是否能新增到  末尾時,都要從頭進行一次比較,而這裡面大多數次的比較在確定  是否能新增到  末尾時就已經進行過了。那麼,我們為什麼不將 長度為  的遞增子序列的最小末尾元素儲存起來,便於下次直接確定  是否能插到該序列末尾呢?        由此,我們可以重新定義 , 設  為長度為 j 的遞增子序列的末尾最小值。這樣的   一定是有序的(為什麼?這點很關鍵,這是時間上進行優化的基礎)。 對於一個新的 , 只需要將  有序插入到  中即可。        如果找得到插入的位置,那麼就另  取代原該位置的元素(比如 ,且一定有 )即可,表示的出現,使得 不再成為長度為  的遞增子序列的最小末尾元素, 因為有更小的  出現。         當找不到插入位置(即  大於任意一個)時,表示  可以新增到任意一個已有遞增子序列末尾構成長度加1的遞增子序列,因此就將  新增到陣列  的末尾。最終陣列  的長度即是原序列中最長遞增子序列的長度。而           複雜度分析:由於陣列 有序,所以插入操作可以使用二分查詢法進行,複雜度是 ,遍歷原陣列的時間是, 所以演算法總的時間複雜度是         演算法實現如下,題目是HDU 1025 :
//longest increment subsquence
#include <iostream>
#define MAXN 500001
using namespace std;
int a[MAXN];
void Insert(int* Min, int& nMin, int x)
{
    int left = 0, right = nMin-1;
    while(left<=right)
    {
        int mid = (left+right)/2;              
        if( Min[mid] < x )
        {
            left = mid+1;
        }
        else if( Min[mid] > x )
        {
             right = mid -1 ;
        }
        else return;                                        
    } 
    Min[left] = x;    
    if(left == nMin) nMin++;
}
int LIS(int n)
{
    int* Min = new int[n];
    int nMin = 1;
    Min[0] = a[0];
    for(int i = 1; i < n; i++)
    {
        Insert(Min, nMin, a[i]);            
    }   
    delete []Min;
    return nMin;
} 

int main()
{
    int n;
    int nCase = 1;
    while(cin>>n)
    {
       int i;
       for(i=1;i<=n;i++)
       {
           int j;
           cin>>j;
           cin>>a[j-1];
       }
       int lenth = LIS(n);
       cout<<"Case "<<nCase++<<":"<<endl<<"My king, at most ";
       if(lenth > 1) cout<<LIS(n)<<" roads can be built."<<endl<<endl;  
       else  cout<<LIS(n)<<" road can be built."<<endl<<endl;        
    }   
    return 0; 
}


補充資料:

       下面的分析截圖來自於CSDN的論壇,不知道出自哪本書,僅供參考。

(四)最大遞增子序列

        最長遞增子序列有一個變形的問題,如HDU 1087,題目意思是求一個序列中,各個元素之和最大的嚴格遞增子序列。解決思路如LIS中的方法2,只不過令 b(j) 表示以 a[j] 結尾的遞增子序列的最大值,最後再從陣列b中找出數值最大的元素即可(也可以記錄一個max變數,這樣省略了一步遍歷找最大值的過程)。        AC程式碼如下:
//hdu 1087
//dynamic programming

#include <iostream>
using namespace std;

int main()
{
   int n;
   while(cin>>n && n)
   {
      int* a = new int[n];
      int* b = new int[n];
      
      for(int i = 0 ; i < n; i++)
      {
              cin>>a[i];
              b[i] = 0 ;
      }
      b[0] = a[0];
      int max = a[0];
      for(int i = 1; i < n; i++)
      {
              b[i] = a[i];
              for(int j = 0 ; j < i ; j++)
              {
                      if(a[i] > a[j] && b[j]+a[i]>b[i]) b[i] = b[j]+a[i];
              }
              if(max < b[i]) max=b[i];   
              /*  
              cout<<"level "<<i<<":";
              for(int j = 0 ; j < n ;j++) cout<<b[j]<<"  ";
              cout<<endl;
              */    
      }
      cout<<max<<endl;                            
   } 
   return 0;   
}

演算法的時間複雜度同LIS問題DP演算法的方法2的時間複雜度。