题目源自《程序员》2001年第8期编程擂台
问题描述
最近出现了一种奇异的生物病毒,这种病毒侵染的范围很广,科学家们经研究发现,这种病毒的内部存在一种环状DNAD片断,而正常情况下,生物的基因总是呈线状排列的.
因此据Dr.X推测,病毒侵染某个生物的几率可能正是与此有关:被侵染生物的DNA中总是或多或少存在着一些片段,它们与环状DNA片断中的一部分是相同的(可称之为 “匹配”),而它们中最长的片断越长,生物被侵染的可能性就越大,其后又有研究发现,某些生物被侵染的几率远大于其它生物,对此Dr.X推测,可能是因为环状DNA片段不仅可以部分地匹配被侵染生物的DNA片断,还可以循环地匹配.
例如如果环状DNA片断为abc(也可以表示为bca或是cab,但它和acb是不同的),被侵染生物的DNA为abbcabcabb,那么能被环状DNA片断匹配的最长片段就是bcabcab,长度为7.
面对大量的实验数据,Dr.X希望你能够帮她设计一个程序,计算出被侵染生物的DNA中与环状DNA片段匹配的最长片段的长度.
输入格式:输入文件名为Virus.in,文件第1行是一个正整数n(n<=1000),表示环状DNA片段的长度.第2行是一长度为n的字符串(由大小写英文字母组成,下同),它描述了环状DNA片段.第3行是一个正整数m(m<=100000),表示被侵染生物的DNA的长度,第4行是一长度为m的字符串,它描述被侵染生物的DNA.
输出格式:输出文件名为Virus.out,文件只需包含一个数,即最长片段的长度.
样例输入 Virus.in 样例输出 Virus.out
3 7
abc
10
abbcabcabb
算法分析:
除去背景信息,不难看出本题的求解目标是一个环串A(即环状DNA片段)和一个线串B(即被侵染生物的DNA)的最长公共子串.
本题可以用动态规划求解:我们用T[i][j]来表示以线串B的第i个字符和环串A的第j个字符结尾的最长公共子串的长度,显然有如下公式:
T[i-1][j-1]+1; A[j]=B[i]且 1<j<=n
T[i][j]= T[i-1][n]+1; A[j]=B[i]且 j=1
0; A[j]!=B[i]
这也是本题的动态转移方程
例如在样例中,环串A是abc,线串B是abbcabcabb,则T如下表所示(空格表示0)
j i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
1 | 1 |
|
|
| 3 |
|
| 6 |
|
|
2 |
| 2 | 1 |
|
| 4 |
|
| 7 | 1 |
3 |
|
|
| 2 |
|
| 5 |
|
|
|
当然在编程实现时,我们是不会用一个100000*1000的表格来动态规划的,由于T[i]只与T[i-1]有关,所以可以保存一个2*1000的表格,空间复杂度为O(n),时间复杂度为O(n*m).另外,逐个拿A[j]与B[i]比较也没有必要,由于字符集为{a,….z,A,……Z},并不太大,所以参考程序用一个索引表记录了每个字符在A中出现的位置,读入B[i]时直接定位即可
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
const int MAX_N=1000;
int m,n,longest=0;
int firstPos[128];
int nextPos[MAX_N];
int dpTable[2][MAX_N];
void init()
{
string st;
memset(firstPos,-1,sizeof(firstPos));
cin>>n;
cin.get();
cin>>st; //读入数据并建立索引
for(int i=n-1;i>=0;i--)
{
char ch=st[i];
nextPos[i]=firstPos[ch];
firstPos[ch]=i;
}
}
void solve()
{
memset(dpTable,0,sizeof(dpTable));
cin>>m;
char prech=0;
for(int i=0;i<m;i++) //动态规划
{
int now=i%2;
int pre=1-now;
char ch;
cin>>ch;
int pos=firstPos[ch];
while(pos>=0)
{
if(pos==0)//动态规划的状态转移方程
dpTable[now][pos]=dpTable[pre][n-1]+1;
else
dpTable[now][pos]=dpTable[pre][pos-1]+1;
if(dpTable[now][pos]>longest)
longest=dpTable[now][pos];
pos=nextPos[pos];
}
pos=firstPos[prech];//为下一次循环作初始化
while(pos>=0)
{
dpTable[pre][pos]=0;
pos=nextPos[pos];
}
prech=ch;
}
cout<<longest<<endl;
}
int main()
{
init();
solve();
return 0;
}
上面的程序中用到了一些建立字符索引的小技巧,其中firstPos数组和nextPos数组用来记录某个字符在病毒DNA中出现的位置,对于一个字符ch,首先根据pos=firstPos[ch]可以得到该字符在病毒DNA中的第一个位置,然后根据pos=nextPos[pos]可以得到下一个位置,当pos=-1时说明已经完全遍历了该字符在病毒DNA中出现的所有位置.在Solve函数中, for(int i=0;i<m;i++)循环用来遍历被侵染生物的DNA的每个字符,dpTable是一个2*n的表格,用来保存动态规划的信息.now表示当前阶段在dpTable中的行,pre表示上一个阶段在dpTable中的行,循环中首先得到被侵染生物的DNA的一个字符ch,然后根据firstPos 和nextPos依次遍历该字符在病毒DNA中的每个位置,并且根据状态转移方程计算对于状态的最优解,For 循环的最后几行:
pos=firstPos[prech];//为下一次循环作初始化
while(pos>=0)
{
dpTable[pre][pos]=0;
pos=nextPos[pos];
}
prech=ch;
prech始终是上一次循环时读入的字符ch,这几行的作用就是将dpTable的第pre行全部清0,这样下次循环时dpTable的第now行就已经初始化为0了.