概述
经典算法
求解 L C S LCS LCS(最长公共子序列)时,一般采用动态规划的方法。
例:有
s
t
r
n
strn
strn与
s
t
r
m
strm
strm两个序列,设
D
P
DP
DP方程
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示
s
t
r
n
strn
strn的前
i
i
i位与
s
t
r
m
strm
strm的前
j
j
j位的LCS长度,转移方程如下:
s
t
r
n
[
i
]
=
=
s
t
r
m
[
j
]
:
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
−
1
]
+
1
strn[i]==strm[j]:f[i][j]=f[i-1][j-1] +1
strn[i]==strm[j]:f[i][j]=f[i−1][j−1]+1
s
t
r
n
[
i
]
!
=
s
t
r
m
[
j
]
:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
−
1
]
,
f
[
i
−
1
]
[
j
]
)
strn[i]!=strm[j]:f[i][j]=max(f[i][j-1],f[i-1][j])
strn[i]!=strm[j]:f[i][j]=max(f[i][j−1],f[i−1][j])
注意:上述两者可以相互独立,即当两字符相等的时候只进行第一步即可。
证明: f [ i − 1 ] [ j − 1 ] + 1 > = m a x ( f [ i ] [ j − 1 ] , f [ i − 1 ] [ j ] ) f[i-1][j-1]+1>=max(f[i][j-1],f[i-1][j]) f[i−1][j−1]+1>=max(f[i][j−1],f[i−1][j])必定成立。
时间复杂度:
O
(
n
m
)
O(nm)
O(nm)
空间复杂度:
O
(
n
m
)
O(nm)
O(nm)
for( int i = 0; i < len_n; i++ )
{
for( int j = 0; j < len_m; j++)
{
if(strn[i] == strm[j])
{
f[i][j] = f[i-1][j-1] + 1;
}
else
{
f[i][j] = max(f[i-1][j], f[i][j-1]);
}
}
}
输出所有的 L C S LCS LCS
要想输出所有的 L C S LCS LCS,我们需要知道对于 i , j i,j i,j阶段的 L C S LCS LCS是由哪里转移而来的,因此需要逆向查找,从而通过递归求出 L C S LCS LCS的转移路径。
设递归函数为 f i n d L C S ( i , j , s t r ) findLCS(i,j,str) findLCS(i,j,str),其中 s t r str str代表着当前存储的 L C S LCS LCS
转移种类有:
- s t r n [ i ] = = s t r m [ j ] strn[i]==strm[j] strn[i]==strm[j],则该阶段一定可以作为一个字符存进 s t r str str中(因为递归是逆向递归,所以现在的选择不会影响之前的选择及方案)
- s t r n [ i ] ! = s t r m [ j ] a n d d p [ i − 1 ] [ j ] < d p [ i ] [ j − 1 ] strn[i]!=strm[j] space and space dp[i-1][j] < dp[i][j-1] strn[i]!=strm[j] and dp[i−1][j]<dp[i][j−1] ,则由 i − 1 , j i-1,j i−1,j转移而来,递归 f i n d L C S ( i − 1 , j , s t r ) findLCS(i-1,j,str) findLCS(i−1,j,str)
- 当大于号同理
- 如果两个相等,则说明可以从两边转移而来,因此需要有两个递归分支
记得在 i , j i,j i,j到达起点后把 s t r str str存进答案中。
set<string> answer;
void findLCS(int i, int j, string str)
{
while (i>0 && j>0)
{
if (strn[i-1] == strm[j-1])
{
str.push_back(strn[i-1]);
--i;
--j;
}
else
{
if (dp[i-1][j] > dp[i][j-1])
--i;
else if (dp[i-1][j] < dp[i][j-1])
--j;
else
{
findLCS(i-1, j, str);
findLCS(i, j-1, str);
return;
}
}
}
answer.insert(Reverse(lcs_str));
}
注意:如果只要求输出其中一个最优解,就只需要把 w h i l e while while去掉,然后把每一个 i f if if条件判断语句里都加上递归入口,去掉 i − − i-- i−−与 j − − j-- j−−。
空间复杂度优化
注意到转移时只与 f [ i − 1 ] [ j − 1 ] 、 f [ i − 1 ] [ j ] 、 f [ i ] [ j − 1 ] f[i-1][j-1]、f[i-1][j]、f[i][j-1] f[i−1][j−1]、f[i−1][j]、f[i][j−1]有关,因此可以尝试滚动数组优化。
设
d
p
[
j
]
dp[j]
dp[j]代表当前循环到
s
t
r
n
strn
strn的第
i
i
i个字符时的答案构成的数组。
在更新
d
p
[
j
]
dp[j]
dp[j]时,即是对应经典算法中的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],则此时
d
p
[
j
]
dp[j]
dp[j]中保留的数据应该是
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j],而
d
p
[
j
−
1
]
dp[j-1]
dp[j−1]保留的数据是
d
p
[
i
]
[
j
−
1
]
dp[i][j-1]
dp[i][j−1]。
那么应该如何获得
d
p
[
i
−
1
]
[
j
−
1
]
dp[i-1][j-1]
dp[i−1][j−1]呢?
可以开一个临时变量
t
e
m
p
temp
temp,保留每一次更新前的
d
p
[
j
]
dp[j]
dp[j](相当于
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]),则当循环进行到第
j
+
1
j+1
j+1个字符时,
t
e
m
p
temp
temp内保留的数据就相当于我们要求的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],故问题得到了解决。
代码如下:
for (i = 0; i < len_n; i++)
{
temp = 0;
for (j = 0; j < len_m; j++)
{
now_val = dp[j];//now_val代表dp[i-1][j]的值
if (strn[i] == strm[j])
dp[j] = temp + 1;//代表dp[i-1][j-1]+1
else
dp[j] = max(dp[j - 1], dp[j]);//dp[j-1]代表dp[i][j-1]
temp = now_val;//保留dp[i-1][j]的值
}
}
空间复杂度: O ( n ) O(n) O(n)
时间复杂度优化
适用条件:序列中重复元素较少,最好为排列
思路:考虑每一个出现在 s t r n strn strn的字符,在 s t r m strm strm中找到其出现的所有位置并加以分类降序储存在数组中。再把每一个字符对应的坐标按照对应字符在 s t r n strn strn中出现的下标**顺序排列成一个新序列 M M M,则 M M M的LIS(最长上升子序列)即是所求答案。
例:
strn=abscsa
strm=adbsccab
则
a在strm的坐标为0、6
b在strm的坐标为2、7
s在strm的坐标为3
c在strm的坐标为4、5
则序列M为
6 0 7 2 3 5 4 3 6 0
LIS长度为5
故LCS长度为5
t i p s : tips: tips:
- 正确性:求出 M M M的最长上升子序列保证了这些字符在 s t r m strm strm中升序出现,又因为这些字符是按照在 s t r n strn strn中的坐标升序排列的,故为两者的 L C S LCS LCS。
- 选择降序排列坐标数组:防止同一个位置的字符被多次选取计算。
- 如何在 O ( n ) O(n) O(n)时间内得到坐标数组:使用链表或者动态数组
void solve(char *str1, char *str2)
{
len_n = strlen(str1), len_m = strlen(str2);
for(int i = 0; i < len_n; i++)
{
inv[str1[i] - ' '] = true;
head[str1[i] - ' '] = -1;
}
for(int i = 0; i < len_m; i++)
{
if(inv[str2[i] - ' '])
{
next[i] = head[str2[i] - ' '];
head[str2[i] - ' '] = i;
}
}
for(int i = 0; i < len_n; i++)
{
int now_pos = head[str1[i] - ' '];
while (now_pos != -1)
{
str[str_len++] = now_pos;//同时实现了降序排序的要求
now_pos = next[now_pos];
}
}
}
上述代码实现了构造序列 M M M(即是代码中的 s t r str str)的任务。
时间复杂度:对于没有重复元素的序列,时间复杂度为 O ( n ) O(n) O(n),随序列中元素重复度升高而升高,最坏可以为 O ( n 2 ) O(n^2) O(n2),即整个序列只有一种元素。
求解 L I S LIS LIS可以使用树状数组优化,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
因此总复杂度较好情况下为 O ( n l o g n ) O(nlogn) O(nlogn)。
最后
以上就是危机墨镜为你收集整理的LCS(最长公共子序列)算法及其优化经典算法输出所有的 L C S LCS LCS空间复杂度优化时间复杂度优化的全部内容,希望文章能够帮你解决LCS(最长公共子序列)算法及其优化经典算法输出所有的 L C S LCS LCS空间复杂度优化时间复杂度优化所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复