最长公共子序列问题,即寻找出给定的两个序列X=<X1,X2, ……,Xn>和Y=<Y1,Y2,……,Ym>的最长的公共部分。
1.一般的动态规划
解题依据:
最长公共子序列问题的最优子结构性质,是指在两个序列的最长公共子序列中,如果最后一个字符相同,则这个**相同的字符一定是最长公共子序列的最后一个字符**,并且剩余部分也是这两个序列去掉最后一个字符后的最长公共子序列。
** 最优子结构的含义**:在最长公共子序列问题中,最优子结构意味着原问题的最优解可以通过求解其子问题的最优解来构建。具体来说,如果我们已经找到了两个序列的一个最长公共子序列,并且在这个子序列的最后一位字符处,两个原序列的对应字符也相同,那么这个公共字符一定包含在最终的最长公共子序列中,并且它之前的字符也构成了一个最长公共子序列。
根据算法导论中给出的方法实现最长公共子序列(备忘录技术,c[i][j]就相当于是我们的备忘录),但是这种方法不能给出所有相同长度最长子序列的情况:
#include <string>
#include <iostream>
using namespace std;
string X, Y;
int c[51][51];
char b[51][51];
void LCS_LENGTH(string X, string Y)
{
int m = X.length();
int n = Y.length();
for (int i = 1; i <= m; i++)
c[i][0] = 0;
for (int j = 0; j <= n; j++)
c[0][j] = 0;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (X[i - 1] == Y[j - 1])
{
c[i][j] = c[i - 1][j - 1] + 1;
b[i][j] = '|'; //↖:直接输入这个符号程序无法识别
}
else if (c[i - 1][j] >= c[i][j - 1])
{
c[i][j] = c[i - 1][j];
b[i][j] = '<'; //↑
}
else
{
c[i][j] = c[i][j - 1];
b[i][j] = '>'; //←
}
}
}
}
void Print_LCS(string X, int i, int j)
{
if (i == 0 || j == 0)
return;
if (b[i][j] == '|')
{
Print_LCS(X, i - 1, j - 1);
cout << X[i - 1];
}
else if (b[i][j] == '<')
Print_LCS(X, i - 1, j);
else
Print_LCS(X, i, j - 1);
}
int main()
{
X = "ABCBDAB";
Y = "BDCABA";
LCS_LENGTH(X, Y);
Print_LCS(X, X.length(), Y.length());
return 0;
}
数据结构的选择:在解决最长公共子序列问题时,通常会使用二维数组c[ ][ ]来记录最长公共子序列的长度,以及另一个二维数组b[ ][ ]来记录这些长度的来源;
初始化过程:对于给定的两个字符串s1和s2,我们会将c[ ][ ]的第一行和第一列初始化为0,这代表空字符串与任何字符串的最长公共子序列长度为0;
递归关系的建立:我们用c[i][j]来记录序列Xi(即s1的前i个字符)和Yj(即s2的前j个字符)的最长公共子序列的长度。当i=0或j=0时,最长公共子序列的长度为0,因为至少有一个序列是空的;
最优子结构的分析:如果Xi和Yj的最后一个字符相同,那么这个字符一定包含在它们的最长公共子序列中。此时,我们可以确定c[i][j]等于c[i-1][j-1] + 1。如果没有匹配的字符,那么c[i][j]将是c[i-1][j]和c[i][j-1]中较大的一个,这表示我们需要在不包含最后一个字符的情况下找到最长的公共子序列。
重叠子问题体现在解决最长公共子序列(LCS)问题时,相同的子问题会被多次遇到和解决。例如,当我们考虑两个序列的前n个字符时,我们可能需要计算它们的前n-1个字符的最长公共子序列,这就是一个重叠的子问题。动态规划通过存储这些子问题的结果来避免重复计算,从而提高效率,即使用‘备忘录’技术。
2.方法一的改进
不使用表b,用O(m+n)的运行时间重构LCS:
#include <string>
#include <iostream>
#include <set>
using namespace std;
int c[51][51];
set<string> output;
void LCS_LENGTH(string X, string Y)
{
int m = X.length();
int n = Y.length();
for (int i = 1; i <= m; i++)
c[i][0] = 0;
for (int j = 0; j <= n; j++)
c[0][j] = 0;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (X[i - 1] == Y[j - 1])
{
c[i][j] = c[i - 1][j - 1] + 1;
}
else if (c[i - 1][j] >= c[i][j - 1])
{
c[i][j] = c[i - 1][j];
}
else
{
c[i][j] = c[i][j - 1];
}
}
}
}
void Print_LCS(string X, string Y, int i, int j, string str)
{
while (i != 0 && j != 0)
{
if (X[i - 1] == Y[j - 1])
{
i--;
j--;
str = X[i] + str;
}
else
{
if (c[i - 1][j] > c[i][j - 1])i--;
else if (c[i - 1][j] < c[i][j - 1])j--;
else
{
Print_LCS(X, Y, i - 1, j, str);
Print_LCS(X, Y, i, j - 1, str);
return;
//出现相同长度的不同公共子序列,各情况互补影响
}
}
}
if (str.length())
output.insert(str);
}
int main()
{
string X, Y, str;
X = "ABCBDAB";
Y = "BDCABA";
LCS_LENGTH(X, Y);
Print_LCS(X, Y, X.length(), Y.length(), str);
if (output.empty())
{
cout << "NO" << endl;
return 0;
}
for (auto it = output.begin(); it != output.end();it++)
{
cout << *it << endl;
}
return 0;
}
如果最长公共子序列只有一种情况,那么函数Print_LCS的运行时间将为O(m+n)
3.用更少的空间复杂度实现LCS_LENGTH
说明如何只使用表c中2×min(m,n)个表项及O(1)的额外空间来计算LCS的长度。然后说明如何只用min(m,n)个表项及O(1)的额外空间完成相同的工作。
#include <string>
#include <iostream>
#include <set>
using namespace std;
int LCS_LENGTH(string X, string Y)
{
//此处使用的数据X比Y长
int temp;//temp用来存储c[i-1][j-1]
int m = X.length();
int n = Y.length();
int* c = new int[n+1];
for (int i = 0; i <= n; i++)
c[i] = 0;
//数组c中储存的相当于是下一行的c[i-1][j]
for (int i = 1; i <= m; i++)
{
temp = c[0];
for (int j = 1; j <= n; j++)
{
if (X[i - 1] == Y[j - 1])
{
temp = temp + 1;
swap(temp, c[j]);
//此时c[j]中储存的相当于是下一个要求元素的c[i-1][j-1];
//因此需要交换temp和c[j]
}
else
{
temp = c[j];//这里的原因和上面相同
if (c[j] < c[j - 1])
c[j] = c[j - 1];
//max(c[i-1][j],c[i][j-1])
}
}
}
return c[n];
}
int main()
{
string X, Y, str;
X = "ABCBDAB";
Y = "BDCABA";
cout<<"最长公共子序列的长度:"<<LCS_LENGTH(X, Y);
return 0;
}
4.求最长递增子序列
4.1 O(n^2)时间复杂度
4.1.1采用LCS的思路求解
例如Y = "BDCABA",求Y的最长上升子序列
这个问题可以转为求Y与其排序后的序列的最长公共子序列:
因为最长上升子序列一定是Y的子集,同时也是排序后序列"AABBCD"的子集。
4.1.2直接用动态规划实现
动态规划:这种方法的基本思想是使用一个数组dp来存储每个元素作为递增子序列末尾时的最大长度。对于数组中的每个元素a[i],我们遍历所有在它之前的a[j](即j < i),找到所有小于a[i]的元素,并更新dp[i]为max(dp[j] + 1,dp[i]),其中j满足a[j] < a[i]。这样,最终dp数组中的最大值就是最长递增子序列的长度。
#include <string>
#include <iostream>
#include <vector>
using namespace std;
int LIS_LENGTH(string X)
{
int n = X.length();
int Max_len=-1;
vector<int> dp(n, 1);
if (X.empty())
return 0;
else
{
for (int i = 1; i < n; i++)
{
for (int j = 0; j < i; j++)
{
if (X[i] > X[j])
{
dp[i] = max(dp[i], dp[j] + 1);
}
}
Max_len = max(Max_len, dp[i]);
}
}
return Max_len;
}
int main()
{
string X = "571946283";
cout<<LIS_LENGTH(X);
return 0;
}
4.2 O(nlogn)时间复杂度
二分查找优化的动态规划:为了优化动态规划中的时间复杂度,可以使用二分查找来减少寻找合适a[j]的时间。这种方法中,我们维护一个数组end,其中end[i]表示长度为i+1的递增子序列的最小结尾元素。通过二分查找,我们可以在对数时间内找到合适的位置来更新end数组,从而将时复杂度降低到O(NlogN)。
#include <string>
#include <iostream>
#include <vector>
using namespace std;
int LIS_LENGTH(vector<int> X)
{
if (X.empty())
return 0;
int n = X.size();
vector<int> end(n);
end[0] = X[0];
int L = 0;
int R = 0;
int left = 0;
int right = 0;
int Max_len = 1;
for (int i = 1; i < n; i++)
{
L = 0;
R = right;
//在这里使用二分查找,可以查找到大于等于X[i]最左边的位置
while (L <= R)
{
int mid = (L + R) / 2;
if (X[i] > end[mid])
L = mid + 1;
else
R = mid - 1;
}
right = max(right, L);//right可能出现-1,即X[i]落在了end数组第一个元素的左边
Max_len = max(Max_len, L+1);//X[i]落在end的数组右边,需要更新Max_len的大小
end[L] = X[i];
}
return Max_len;
}
int main()
{
vector<int> X = {1,4,7,5,6};
cout<<LIS_LENGTH(X);
return 0;
}
参考书籍:《算法导论》——托马斯·科尔曼
参考文章1:http://t.csdnimg.cn/6qIln
参考文章2:http://t.csdnimg.cn/wGkos
参考文章3:http://t.csdnimg.cn/xlQKj
喜欢的小伙伴还请点赞收藏,(❁´◡`❁)( o=^•ェ•)o ┏━┓
版权归原作者 Fatunlorey 所有, 如有侵权,请联系我们删除。