设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk},
记: Xk为序列X中前k个连续字符组成的子序列,
Yk为序列Y中前k个连续字符组成的子序列,
Zk为序列Z中前k个连续字符组成的子序列,
显然有下式成立:
(1)若xm=yn,则zk=xm=yn,且Zk-1是Xm-1和Yn-1的最长公共子序列;
(2)若xm≠yn且zk≠xm,则Z是Xm-1和Y的最长公共子序列;
(3)若xm≠yn且zk≠yn,则Z是X和Yn-1的最长公共子序列。
可见,两个序列的最长公共子序列包含了这两个序列的前缀序列的最长公共子序列。
要找出序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列,可按下述递推方式计算:
当xm=yn时,找出Xm-1和Yn-1的最长公共子序 列,然后在其尾部加上xm即可得到X和Y的最长公共子序列;
当xm≠yn时,必须求解两个子问题:找出Xm-1和Y的最长公共子序列以及Xm和Yn-1 的最长公共子序列,这两个公共子序列中的较长
者即为X和Y的最长公共子序列
设L[i][j]表示子序列Xi和Yj的最长公共子序列的长度,可得如下动态规划函数:
L[0][0] = L[i][0] = L[0][j] = 0 (1≤i≤m,1≤j≤n)
L[i][j]只是记录子序列的长度,要打印得到Xm和Yn具体的最长公共子序列,设二维表S[m+1][n+1],其中S[i][j]表示在计算L[i][j]的过程中的搜索状态,并且有:
若S[i][j]=1,表明ai=bj,则下一个搜索方向是S[i-1][j-1];
若S[i][j]=2,表明ai≠bj且L[i][j-1]≥L[i-1][j],则下一个搜索方向是S[i][j-1];
若S[i][j]=3,表明ai≠bj且L[i][j-1]<L[i-1][j],则下一个搜索方向是S[i-1][j]。
度和状态。举例:序列X=(a,b,c,b,d,b),Y=(a,c,b,b,a,b, d, b,b),建立两个(m+1)×(n+1)的二维表L和表S,分别存放搜索过程中得到的子序列的长
//算法c++描述——最长公共子序列问题
int CommonOrder(int m, int n, int x[ ], int y[ ], int z[ ])
{
for (j=0; j<=n; j++) //初始化第0行
L[0][j]=0;
for (i=0; j<=m; i++) //初始化第0列
L[i][0]=0;
for (i=1; i<=m; i++)
for (j=1; j<=n; j++)
if (x[i]= =y[j]) { L[i][j]=L[i-1][j-1]+1; S[i][j]=1; }
else if (L[i][j-1]>=L[i-1][j]) { L[i][j]=L[i][j-1]; S[i][j]=2; }
else {L[i][j]=L[i-1][j]; S[i][j]=3; }
i=m; j=n; k=L[m][n];
for (i>0 && j>0)
{
if (S[i][j]= =1) { z[k]=x[i]; k--; i--; j--; }
else if (S[i][j]= =2) j--;
else i--;
}
return L[m][n];
}
算法分析:在算法中,
第一个for循环的时间性能是O(n);
第二个for循环的时间性能是O(m);
第三个循环是两层嵌套的for循环,其时间性能是O(m×n);
第四个for循环的时间性能是O(k),而k≤min{m,n},所以,算法的时间复杂性是O(m×n)。
(1)转化为LCS问题
先把序列 L 按照从小到大的顺序排列, 得到另一个序列S,再求L和S的最长公共子序列
(2)动态规划0(n*n)
以i结尾的序列的最长递增子序列和其[0, i - 1]“前缀”的最长递增子序列有关,设LIS[i]保存以i结尾的最长递增子序列的长度:
若i = 0,则LIS[i] = 1;
若i > 0,则LIS[i]的值和其[0, i - 1]前缀的最长递增子序列长度有关,用j遍历[0, i - 1]得到其最长递增子序列为LIS[j],对每一个LIS[j],如果序列array[j] < array[i]并且LIS[j] + 1 > LIS[i],则LIS[i]的值变成LIS[j] + 1。即:
LIS[i] = max{1, LIS[j] + 1},其中array[i] > array[j] 且 j = [0, i - 1]。
代码如下所示。
int lis(int[] array) {
int lis[] = new int[array.length];
for (int i = 0; i < array.length; i++) {
lis[i] = 1;
for (int j = 0; j < i; j++) {
//if (array[j] < array[i] && (lis[j] + 1 > lis[i]))//等号有没有视情况而定
if (array[i] > array[j])// 等号有没有视情况而定
lis[i] = lis[j] + 1 > lis[i] ? lis[j] + 1 : lis[i];
}
}
int max = 0;
for (int k = 0; k < lis.length; k++) {
if (lis[k] > max)
max = lis[k];
}
return max;
}
很显然实践复杂度是O(N*N),那么有没有更快的算法呢?按照正常的思路更快的复杂度应该就是O(N*logN),那么就要涉及到二分了。
(2)二分查找+动态规划实现
假设存在一个序列d[1...9] = 2 1 5 3 6 4 8 9 7,可以看出它的LIS长度是5。
下面一步一步试着找到它。
我们定义一个序列B,然后令i = 1 to 9逐个考察这个序列。
此外,我们用一个变量len来记录现在的最长算到多少。
首先,把d[1]有序的放到B中,令B[1] = 2,就是说当只有一个数字2的时候,长度为1的LIS的最小末尾是2,这时len = 1;
然后,把d[2]有序的放到B中,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1] = 2已经没用了,很容易理解吧,这时len = 1;
接着,d[3] = 5,d[3] > B[1],所以令B[1 + 1] = B[2] = d[3] = 5,就是说长度为2的LIS的最小末尾是5,很容易理解吧,这时B[1...2] = 1, 5,len = 2;
再来,d[4] = 3,它正好在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时B[1...2] = 1,3,len = 2;
继续,d[5] = 6,它在3的后面,因为B[2] = 3,而6在3后面,于是很容易推知B[3] = 6,这时B[1...3] = 1,3,6,还是很容易理解吧?这时len = 3;
第6个,d[6] = 4,你看它在3和6之间,于是就可以把6替换掉,得到B[3] = 4。B[1...3] = 1,3,4,这时len = 3;
第7个,d[7] = 8,它很大,比4大,于是B[4] = 8,这时len = 4;
第8个,d[8] = 9,得到B[5] = 9,len继续增大,这时len = 5;
最后一个,d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] = 7, B[1...5] = 1,3,4,7,9,len = 5。
于是我们知道了LIS的长度为5。
注意,注意。这个1,3,4,7,9不是LIS,它只是存储了对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这个数组数据没有什么意义,但是如果后面再出现两个数字8和9,那么就可以把8更新到d[5],9更新到d[6],得到LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且进行替换而不需要移动——也就是说,可以使用二分查找,将每一个数字的插入时间优化到O(logn),于是算法的时间复杂度就降低到了O(nlogn)了。
代码如下:
#include
#include
#include
#define N 9 //数组元素个数
int array[N] = {2, 1, 6, 3, 5, 4, 8, 7, 9}; //原数组
int B[N]; //在动态规划中使用的数组,用于记录中间结果,其含义三言两语说不清,请参见博文的解释
int len; //用于标示B数组中的元素个数
int LIS(int *array, int n); //计算最长递增子序列的长度,计算B数组的元素,array[]循环完一遍后,B的长度len即为所求
int BiSearch(int *b, int len, int w); //做了修改的二分搜索算法
int main()
{
printf("LIS: %d\n", LIS(array, N));
int i;
for(i=0; i B[len-1]) //如果大于B中最大的元素,则直接插入到B数组末尾
{
B[len] = array[i];
++len;
}
else
{
pos = BiSearch(B, len, array[i]); //二分查找需要插入的位置
B[pos] = array[i];
}
}
return len;
}
//修改的二分查找算法,返回数组元素需要插入的位置。
int BiSearch(int *b, int len, int w)
{
int left = 0, right = len - 1;
int mid;
while (left <= right)
{
mid = left + (right-left)/2;
if (b[mid] > w)
right = mid - 1;
else if (b[mid] < w)
left = mid + 1;
else //找到了该元素,则直接返回
return mid;
}
return left;//数组b中不存在该元素,则返回该元素应该插入的位置
}
参考:http://blog.chinaunix.net/uid-26548237-id-3757779.html
http://qiemengdao.iteye.com/blog/1660229