原题地址:

http://acm.hdu.edu.cn/showproblem.php?pid=1758

 

Problem Description
Though Yueyue and Lele are brothers , they are very different.
For example, Yueyue is very hard in study, especially in writing compositions. To make the composition looks nice , he will not use the same word twice. While Lele is very lazy, and he sometimes copys his brother's homework.

Last week, their teacher asked them to write a composition named "My Mother", they handed the same composition. The teacher was very angry , but Lele just answered "We have the same mother , why should our compositions be different ?"

Now,the teacher is asking Yueyue and Lele to write the compositions again, and he wants to calculate the length of longest common subsequence of words occuring in the two compositions. Can you help him ?


Input
There will be many test cases in the problem.Please process to the end of file.
Each case contains two lines.
The first line means Yueyue's composition and the second line means Lele's composition.
Each composition will contains no more than 10^4 words . And each word will contains less than 40 characters.Each character will only be in a~z or A~Z.
Two words will be separated by a blank.

To make the problem easier, there will be a "#" at the end of each composition.

 

很容看出用HASH+LCS;

对于m,n<=10000  O(n2)超时。

 

View Code
 1 #include<iostream>
 2 using namespace std;
 3 const int MOD=4;
 4 int x[11111],y[11111];
 5 char str[44];
 6 int c[11111];
 7 int c_[111111];
 8 unsigned int seed=131;
 9 inline unsigned int BKDRHash(char *str)
10 {
11          
12          unsigned int hash=0;
13          while(*str){hash=hash*seed+(*str++);}
14          return (hash & 0x7FFFFFFF);
15 }
16 int main()
17 {
18     int i,j,m,n,temp1,temp2;
19     while(1)
20     {
21             i=1;j=1;
22             while(1){scanf("%s",str);if(str[0]=='#')break;x[i++]=BKDRHash(str);}
23             while(1){scanf("%s",str);if(str[0]=='#')break;y[j++]=BKDRHash(str);}  
24     
25     m=i-1;n=j-1;
26     memset(c,0,sizeof(c));
27     memset(c_,0,sizeof(c_));
28     for(i=1;i<=m;i++)
29         for(j=1;j<=n;j++)
30         {
31            if(x[i]==y[j])c[j]=c_[j-1]+1;
32            else if(c[j]<c[j-1])c[j]=c[j-1];
33            c_[j-1]=c[j-1];
34         }
35     printf("%d\n",c[n]);
36     char temp;
37     temp=getchar();
38     if(temp==EOF)break;
39     }
40     
41 }

我们先开始了解LIS(Longest Increasing Subsequence)最长上升(不下降)子序列 。

同样有两种算法复杂度为O(n*logn)和O(n^2);

这里给出O(n*logn)的算法:

View Code
 1 int LIS(int str[],int len)
 2 {
 3     int max,left,right,i,mid;
 4     lis[1] = str[0];
 5     max = 1;
 6     for(i=1; i<len; i++)
 7     {
 8         if(str[i] > lis[max])
 9         {
10             lis[++max] = str[i];
11         }
12         else
13         {
14             left = 1;
15             right = max;
16             while(left <= right)
17             {
18                 mid = (left+right)/2;
19                 if(str[i] > lis[mid])
20                     left = mid + 1;
21                 else if(str[i] < lis[mid])
22                     right = mid - 1;
23                 else
24                     break;
25             }
26             lis[left] = str[i];
27         }
28     }
29     return max;
30 }

另一种写法(加深理解)

View Code
#include <iostream>
using namespace std;

#define  MAX_INPUT_NUM 10000
int limit[MAX_INPUT_NUM];

int main()
{
    int inputNum;
    int curNum;
    int tempBegin;
    int tempEnd;
    int tempMiddle;
    int len;
    
    while(cin>>inputNum){
        len=0;//the count
        for(int i=0;i<inputNum;i++){
            cin>>curNum;

            tempEnd=len;
            tempBegin=1;
            tempMiddle=0;

            while(tempBegin<=tempEnd){
                tempMiddle=(tempBegin+tempEnd)>>1;
                limit[tempMiddle] >=curNum ? tempEnd=tempMiddle-1 : tempBegin=tempMiddle+1;
            }
            limit[tempBegin]=curNum;//first position whose value >=curNum ,if no existed ,is end+1
            tempBegin >len ? len++ :NULL;
    
        }//end for ..
        cout<<len<<endl;
    }//end while

    return 0;
}

 

设A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F[t] = 0(t = 1, 2, ..., len(A))。则有动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, ..., t - 1, 且A[j] < A[t])。
  现在,我们仔细考虑计算F[t]时的情况。假设有两个元素A[x]和A[y],满足
  (1)y <x < t (2)A[x] <A[y] < A[t] (3)F[x] = F[y] 
  此时,选择F[x]和选择F[y]都可以得到同样的F[t]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
  很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] ... A[t-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。
  再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[t] = k的所有A[t]中的最小值。设D[k]记录这个值,即D[k] = min{A[t]} (F[t] = k)。
  注意到D[]的两个特点:
  (1) D[k]的值是在整个计算过程中是单调不上升的。
  (2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
  利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[t]与D[len]。若A[t] > D[len],则将A[t]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = A[t];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[t]。令k = j + 1,则有D[j] < A[t] <= D[k],将A[t]接在D[j]后将得到一个更长的上升子序列,同时更新D[k] = A[t]。最后,len即为所要求的最长上升子序列的长度。
  在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!

 

View Code
#include<iostream>
#include<map>
using namespace std;
int x[10001],y[10001];
char str[45];
unsigned int seed=131;
int lis[10001];

int main()
{
    int i,j,m,n,max,left,right,mid;;
    while(1)
    {
            i=1;j=1;
            map<string,int> map1;
            while(1){scanf("%s",str);if(str[0]=='#')break;x[i]=i;string s(str);map1.insert(pair<string,int>(s,i));i++;}
            
            map<string,int>::iterator iter;
            
            while(1){scanf("%s",str);if(str[0]=='#')break;string s(str);iter=map1.find(s);if(iter!=map1.end())y[j++]=iter->second;}  
            
            m=i-1;n=j-1;
            if(n==0){cout<<0<<endl;char temp;temp=getchar();if(temp==EOF)break;continue;}
            
            lis[1] = y[1];
            max = 1;
            for(i=2; i<=n; i++)
            {
            if(y[i] > lis[max])
            {
             lis[++max] = y[i];
             }
             else
             {
             left = 1;
             right = max;
             while(left <= right)
             {
                 mid = (left+right)/2;
                 if(y[i] > lis[mid])
                     left = mid + 1;
                 else if(y[i] <lis[mid])
                     right = mid - 1;
                 else
                     {left=right=mid;break;}
             }
             lis[left] =y[i];
             
         }
     }
     printf("%d\n",max);

    char temp;
    temp=getchar();
    if(temp==EOF)break;
    }
    
}

 

 (程序未AC,求网友指教)

LCS 的经典方程大家都知道:
设两个序列为 a、b,用 f[i, j] 表示第一个序列的前 i 个数,和第二个序列的前 j 个数的最长公共子序列的长度。
若 a[i] = b[j],则
f[i, j] = max{f[i - 1, j - 1] + 1, f[i, j - 1], f[i - 1, j]}
若 a[i] <> b[j],则
f[i, j] = max{f[i, j - 1], f[i - 1, j]}
边界条件:
f[0, j] = 0
f[i, 0] = 0

但是这样的算法是 O(n^2) 的,对于高达 100000 的 n,无论是时间还是空间都无法满足要求,虽然可以采用滚动数组解决空间问题,但是时间问题还是不能解决。

众所周知一般 LCS 问题的最优的时间复杂度即为 O(n^2),那还有什么可以优化的余地呢?这时,我们要利用数据的稀疏性来进行优化。

观察上面的方程,我们发现,只有当 a[i] = b[j] 的时候 f 的值才会增加。而原数据是 n 的两个排列,因而只有 n 个相等点!

抓住这一点我们就可以优化动态规划方程。我们设在第一个序列中与 a[i] 相等的 b[j] 点的下标为 pair[i],那么有:

f[i, pair[i]] = max{f[j, pair[j]} (j <= i, pair[j] < pair[i])

这样,我们只需要保存出长度为 i 的公共子序列的最小下标 j,再用二分查找维护即可。

空间复杂度 O(n),时间复杂度 O(nlgn)。

posted on 2012-05-09 16:00  leonaiverson  阅读(233)  评论(0编辑  收藏  举报