【概念区分】
对一个序列X{A,C,B,D,F}和另一个序列Y{A,F,B,D,K},两者的最长公共序列是{A,B,D},两者最长的公共子串是{B,D}
最长公共子串要求连续
【动态规划实现解析】
用动态规划实现最重要的是要找到递归方程或者迭代方程,只要把这个方程找出来了,对着这个方程想个几十分钟,就可以把代码写出来,然后再拿例子测试,写出来的代码基本就没问题了。
也许你第一次接触“如何将方程转化为代码”的问题,感觉无从下手,这很正常。只要写得多了,将方程转化为代码其实并不困难。
困难的是如何根据问题找到那个方程,这涉及到将问题抽象为数学问题。
求最长公共子序列的递归方程如下,那么这个方程是如何得出来的呢?
求递归方程和迭代方程很重要的一点是假设我们获得了在某一条件下的结果,在这一结果的基础上求得下一条件下的结果。(即要求某一条件下的结果,已知上一条件下的结果)
例如,在求长度为m的序列X{A,C,B,D,F}和长度为n的序列Y{A,F,B,D,K}的最长公共子序列长度C[m,n]时,可以假设它们的上一条件下的结果是已知的。
下面我们要开始穷举上一条件下的所有结果,在穷举这些结果的基础上给出方程。
这里有两个变量,序列X的长度m和序列Y的长度n,所以上一条件下的结果有三个m-1,n-1,m-1且n-1。由这三个结果可以推出其他所有结果。
当m-1时,序列X要增加一个新的字符,只有当新增加的字符为K的时候(准确来说或是Y中D以后的字符,即Xm=Yd),最长公共子序列的长度C才会加1,否则长度C不变。那么C[m,n]>=C[m-1,n]。
当n-1时,序列Y要增加一个新的字符,只有当新增加的字符为F的时候(Xd=Yn),最长公共子序列的长度C才会加1,否则长度C不变。那么C[m,n]>=C[m,n-1]。
当m-1,n-1时,有以下可能:序列X和序列Y增加了相同的字符(Xm=Yn),那么长度C加1;序列X增加了K,序列Y增加了F,那么长度C加1,相当于m-1时或者相当于n-1时;序列X增加了K,序列Y增加的不是F,长度C加1,相当于m-1时;序列X增加的不是K,序列Y增加是F,那么长度C加1,相当于n-1时;序列X增加的不是K,序列Y增加的不是F,那么长度C不变。那么C[m,n]>=C[m-1,n-1]。
所以有:
如果Xm=Yn,则C[m,n]=C[m-1,n-1]+1;如果Xm≠Yn,C[m,n]=C[m-1,n]或者C[n-1,m]中大的那一个,如果两者相等,随便选取一个。考虑空序列,那么m=0或者n=0时,C[m,n]=0。
【最长公共子序列长度】
上文中m和n表示字符串的长度,对C[m,n]的二维数组来说,其大小实际上是m+1,n+1。C[m,n]的值就是最长公共子序列的长度。
【输出最长公共子序列】
创建一个标记二维数据mark,如果Xm=Yn,表示Xm或者Yn是最长公共子序列的一个字符,那么mark[m,n]=1。如果C[m-1,n]>=C[m,n-1]时,mark[m,n]=2。C[m-1,n]<C[m,n-1]时,mark[m,n]=3。
【输出所有最长公共子序列】
当C[m-1,n]=C[n-1,m]时,可以随机选取一个,这就造成了路径的差别,即可以由C[m-1,n]向右加1到C[m,n],也可以由C[m,n-1]向下加1到C[m,n]。反之,在C[m,n]处相当于有了个路口,可以向左走到C[m-1,n],也可以向上走到C[m,n],我们要把这个路口标记下来。先向左走,走到头后(即m=0或者n=0)回到起点,再经过这个路口后向右走。
【求最长公共子串】
最长公共子串要求连续,那么如果Xm≠Yn,则子串就断开了,我们把这里标记为0。找到二维数组C中的最大值,即为最长公共子串的长度。标记最大值时的m或者n,就知道子串的最后一个字符位于在序列X或者Y中的位置。
【代码实现】
using System;
using System.Collections.Generic;
namespace LCS
{
class Program
{
static void Main(string[] args)
{
Program pr = new Program();
string str1 = "ABCPDSFJGODIHJOFDIUSHGD";
string str2 = "OSDIHGKODGHBLKSJBHKAGHI";
string str3 = "ABCBDAB";
string str4 = "BDCABA";
string str5 = "ABDCDBABB";
string str6 = "ADCDADB";
//求最长公共子序列的长度并输出
Stack<char> lcs;
int length = pr.LongestCommonSubsequence(str1, str2,out lcs);
Console.WriteLine(length);
foreach (char lc in lcs)
{
Console.Write(lc);
}
Console.WriteLine(" ");
//输出所有最长公共子序列
pr.PrintAllLcs(str3,str4);
Console.WriteLine(" ");
string str;
//求最长公共子串长度并输出
int len = pr.LongestCommonSubstring(str5, str6,out str);
Console.WriteLine(len+" "+str);
Console.ReadKey();
}
public int LongestCommonSubsequence(string str1, string str2,out Stack<char> lcs )
{
if (str1 == "" || str2 == "")
{
lcs = null;
return 0;
}
int m = str1.Length + 1;
int n = str2.Length + 1;
int[,] c=new int[m, n];
int[,] mark=new int[m, n];
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
if (str1[i - 1] == str2[j - 1])//注意str1中第i个字符对应的索引为i-1
{
c[i, j] = c[i - 1, j - 1] + 1;
mark[i, j] = 1;
}
else if(c[i-1,j]>=c[i,j-1])//两者相等时取左边的
{
c[i, j] = c[i - 1, j];
mark[i, j] = 2;
}
else
{
c[i, j] = c[i, j - 1];
mark[i, j] = 3;
}
}
}
lcs=new Stack<char>();
for (int i = m-1,j=n-1; i >=0&&j>=0;)
{
if (mark[i, j] == 1)
{
i--;
j--;
lcs.Push(str1[i]);
}
else if(mark[i, j] == 2)
{
i--;
}
else
{
j--;
}
}
return c[str1.Length, str2.Length];
}
public void PrintAllLcs(string str1, string str2)
{
if (str1 == "" || str2 == "")
{
return;
}
int m = str1.Length + 1;
int n = str2.Length + 1;
int[,] c = new int[m, n];
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
if (str1[i - 1] == str2[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];
}
}
}
Stack<char> lcs = new Stack<char>();
Dictionary<int,bool> mark=new Dictionary<int, bool>();
for (int i = m - 1, j = n - 1; i >= 1 && j >= 1;)
{
if (str1[i-1] == str2[j-1])
{
i--;
j--;
lcs.Push(str1[i]);
}
else if (c[i - 1, j] > c[i, j - 1])//向左走
{
i--;
}
else if (c[i - 1, j] < c[i, j - 1]) //向上走
{
j--;
}
else
{
if (mark.ContainsKey(i))//如果包含位置i,表示再次来到了这个位置
{
j--;
if(mark[i] == false)//false表示在这个位置还没有向上走,向上走了设为true
mark[i]=true;
}
else
{
mark.Add(i, false);//用字典将i位置存起来,然后向左走
i--;
}
}
if (i < 1 || j < 1)
{
bool reset=false;
foreach (KeyValuePair<int, bool> keyValuePair in mark)
{
if (!keyValuePair.Value)
reset = true;
}
if(reset)//只要有一个位置的路口没有为true,就表明没有走完,回到起点重新走
{
i = m - 1;
j = n - 1;
lcs.Push(' ');
}
}
}
foreach (char lc in lcs)
{
Console.Write(lc);
}
}
public int LongestCommonSubstring(string str1, string str2, out string str)
{
str = null;
if (str1 == "" || str2 == "")
{
return 0;
}
int m = str1.Length + 1;
int n = str2.Length + 1;
int[,] c = new int[m, n];
int maxValue=0;
int flag=0;
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
if (str1[i - 1] == str2[j - 1])
{
c[i, j] = c[i - 1, j - 1] + 1;
if (c[i, j] > maxValue)
{
maxValue = c[i, j];
flag = i;
}
}
else
{
c[i, j] = 0;
}
}
}
int num = maxValue;
while (num > 0)
{
str = str1[flag - num]+ str;
num--;
}
return maxValue;
}
}
}
【输出结果】
【参考资料】
https://blog.csdn.net/u013074465/article/details/45392687
https://blog.csdn.net/weixin_40673608/article/details/84262695