经典算法题每日演练——第四题 最长公共子序列

原文:经典算法题每日演练——第四题 最长公共子序列

一: 作用

最长公共子序列的问题常用于解决字符串的相似度,是一个非常实用的算法,作为码农,此算法是我们的必备基本功。

二:概念

举个例子,cnblogs这个字符串中子序列有多少个呢?很显然有27个,比如其中的cb,cgs等等都是其子序列,我们可以看出

子序列不见得一定是连续的,连续的那是子串。

我想大家已经了解了子序列的概念,那现在可以延伸到两个字符串了,那么大家能够看出:cnblogs和belong的公共子序列吗?

在你找出的公共子序列中,你能找出最长的公共子序列吗?

从图中我们看到了最长公共子序列为blog,仔细想想我们可以发现其实最长公共子序列的个数不是唯一的,可能会有两个以上,

但是长度一定是唯一的,比如这里的最长公共子序列的长度为4。

三:解决方案

<1> 枚举法

这种方法是最简单,也是最容易想到的,当然时间复杂度也是龟速的,我们可以分析一下,刚才也说过了cnblogs的子序列

个数有27个 ,延伸一下:一个长度为N的字符串,其子序列有2N个,每个子序列要在第二个长度为N的字符串中去匹配,匹配一次

需要O(N)的时间,总共也就是O(N*2N),可以看出,时间复杂度为指数级,恐怖的令人窒息。

<2> 动态规划

既然是经典的题目肯定是有优化空间的,并且解题方式是有固定流程的,这里我们采用的是矩阵实现,也就是二维数组。

第一步:先计算最长公共子序列的长度。

第二步:根据长度,然后通过回溯求出最长公共子序列。

现有两个序列X={x1,x2,x3,...xi},Y={y1,y2,y3,....,yi},

设一个C[i,j]: 保存Xi与Yj的LCS的长度。

递推方程为:

不知道大家看懂了没?动态规划的一个重要性质特点就是解决“子问题重叠”的场景,可以有效的避免重复计算,根据上面的

公式其实可以发现C[i,j]一直保存着当前(Xi,Yi)的最大子序列长度。

 1 using System;
 2 namespace ConsoleApplication2
 3 {
 4     public class Program
 5     {
 6         static int[,] martix;
 7
 8         static string str1 = "cnblogs";
 9         static string str2 = "belong";
10
11         static void Main(string[] args)
12         {
13             martix = new int[str1.Length + 1, str2.Length + 1];
14
15             LCS(str1, str2);
16
17             //只要拿出矩阵最后一个位置的数字即可
18             Console.WriteLine("当前最大公共子序列的长度为:{0}", martix[str1.Length, str2.Length]);
19
20             Console.Read();
21         }
22
23         static void LCS(string str1, string str2)
24         {
25             //初始化边界,过滤掉0的情况
26             for (int i = 0; i <= str1.Length; i++)
27                 martix[i, 0] = 0;
28
29             for (int j = 0; j <= str2.Length; j++)
30                 martix[0, j] = 0;
31
32             //填充矩阵
33             for (int i = 1; i <= str1.Length; i++)
34             {
35                 for (int j = 1; j <= str2.Length; j++)
36                 {
37                     //相等的情况
38                     if (str1[i - 1] == str2[j - 1])
39                     {
40                         martix[i, j] = martix[i - 1, j - 1] + 1;
41                     }
42                     else
43                     {
44                         //比较“左边”和“上边“,根据其max来填充
45                         if (martix[i - 1, j] >= martix[i, j - 1])
46                             martix[i, j] = martix[i - 1, j];
47                         else
48                             martix[i, j] = martix[i, j - 1];
49                     }
50                 }
51             }
52         }
53     }
54 }

图大家可以自己画一画,代码完全是根据上面的公式照搬过来的,长度的问题我们已经解决了,这次要解决输出最长子序列的问题,

我们采用一个标记函数Flag[i,j],当

①:C[i,j]=C[i-1,j-1]+1  时 标记Flag[i,j]="left_up";    (左上方箭头)

②:C[i-1,j]>=C[i,j-1]   时 标记Flag[i,j]="left";          (左箭头)

③: C[i-1,j]<C[i,j-1]     时 标记Flag[i,j]="up";            (上箭头)

例如:我输入两个序列X=acgbfhk,Y=cegefkh。

 1 using System;
 2
 3 namespace ConsoleApplication2
 4 {
 5     public class Program
 6     {
 7         static int[,] martix;
 8
 9         static string[,] flag;
10
11         static string str1 = "acgbfhk";
12
13         static string str2 = "cegefkh";
14
15         static void Main(string[] args)
16         {
17             martix = new int[str1.Length + 1, str2.Length + 1];
18
19             flag = new string[str1.Length + 1, str2.Length + 1];
20
21             LCS(str1, str2);
22
23             //打印子序列
24             SubSequence(str1.Length, str2.Length);
25
26             Console.Read();
27         }
28
29         static void LCS(string str1, string str2)
30         {
31             //初始化边界,过滤掉0的情况
32             for (int i = 0; i <= str1.Length; i++)
33                 martix[i, 0] = 0;
34
35             for (int j = 0; j <= str2.Length; j++)
36                 martix[0, j] = 0;
37
38             //填充矩阵
39             for (int i = 1; i <= str1.Length; i++)
40             {
41                 for (int j = 1; j <= str2.Length; j++)
42                 {
43                     //相等的情况
44                     if (str1[i - 1] == str2[j - 1])
45                     {
46                         martix[i, j] = martix[i - 1, j - 1] + 1;
47                         flag[i, j] = "left_up";
48                     }
49                     else
50                     {
51                         //比较“左边”和“上边“,根据其max来填充
52                         if (martix[i - 1, j] >= martix[i, j - 1])
53                         {
54                             martix[i, j] = martix[i - 1, j];
55                             flag[i, j] = "left";
56                         }
57                         else
58                         {
59                             martix[i, j] = martix[i, j - 1];
60                             flag[i, j] = "up";
61                         }
62                     }
63                 }
64             }
65         }
66
67         static void SubSequence(int i, int j)
68         {
69             if (i == 0 || j == 0)
70                 return;
71
72             if (flag[i, j] == "left_up")
73             {
74                 Console.WriteLine("{0}: 当前坐标:({1},{2})", str2[j - 1], i - 1, j - 1);
75
76                 //左前方
77                 SubSequence(i - 1, j - 1);
78             }
79             else
80             {
81                 if (flag[i, j] == "up")
82                 {
83                     SubSequence(i, j - 1);
84                 }
85                 else
86                 {
87                     SubSequence(i - 1, j);
88                 }
89             }
90         }
91     }
92 }

由于直接绘图很麻烦,嘿嘿,我就用手机拍了张:

好,我们再输入两个字符串:

1         static string str1 = "abcbdab";
2
3         static string str2 = "bdcaba";

通过上面的两张图,我们来分析下它的时间复杂度和空间复杂度。

时间复杂度:构建矩阵我们花费了O(MN)的时间,回溯时我们花费了O(M+N)的时间,两者相加最终我们花费了O(MN)的时间。

空间复杂度:构建矩阵我们花费了O(MN)的空间,标记函数也花费了O(MN)的空间,两者相加最终我们花费了O(MN)的空间。

时间: 2024-09-29 11:05:26

经典算法题每日演练——第四题 最长公共子序列的相关文章

经典算法题每日演练——第五题 字符串相似度

原文:经典算法题每日演练--第五题 字符串相似度 这篇我们看看最长公共子序列的另一个版本,求字符串相似度(编辑距离),我也说过了,这是一个非常实用的算法,在DNA对比,网 页聚类等方面都有用武之地. 一:概念 对于两个字符串A和B,通过基本的增删改将字符串A改成B,或者将B改成A,在改变的过程中我们使用的最少步骤称之为“编辑距离”. 比如如下的字符串:我们通过种种操作,痉挛之后编辑距离为3,不知道你看出来了没有? 二:解析 可能大家觉得有点复杂,不好理解,我们试着把这个大问题拆分掉,将"字符串

经典算法题每日演练——第十一题 Bitmap算法

原文:经典算法题每日演练--第十一题 Bitmap算法 在所有具有性能优化的数据结构中,我想大家使用最多的就是hash表,是的,在具有定位查找上具有O(1)的常量时间,多么的简洁优美, 但是在特定的场合下: ①:对10亿个不重复的整数进行排序. ②:找出10亿个数字中重复的数字. 当然我只有普通的服务器,就算2G的内存吧,在这种场景下,我们该如何更好的挑选数据结构和算法呢? 一:问题分析 这年头,大牛们写的排序算法也就那么几个,首先我们算下放在内存中要多少G: (10亿 * 32)/(1024*

经典算法题每日演练——第三题 猴子吃桃

原文:经典算法题每日演练--第三题 猴子吃桃 猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾就多吃了一个.第二天早上又将剩下的桃子吃了一半,还是不过瘾又多 吃了一个.以后每天都吃前一天剩下的一半再加一个.到第10天刚好剩一个.问猴子第一天摘了多少个桃子? 分析: 这是一套非常经典的算法题,这个题目体现了算法思想中的递推思想,递归有两种形式,顺推和逆推,针对递推,只要 我们找到递推公式,问题就迎刃而解了. 令S10=1,容易看出 S9=2(S10+1), 简化一下 S9=2S10+2 S8=2S

经典算法题每日演练——第二十题 三元组

原文:经典算法题每日演练--第二十题 三元组 我们知道矩阵是一个非常强大的数据结构,在动态规划以及各种图论算法上都有广泛的应用,当然矩阵有着不足的地方就是空间和时间 复杂度都维持在N2上,比如1w个数字建立一个矩阵,在内存中会占用1w*1w=1亿的类型空间,这时就会遇到outofmemory...那么面 临的一个问题就是如何来压缩矩阵,当然压缩的方式有很多种,这里就介绍一个顺序表的压缩方式:三元组. 一:三元组 有时候我们的矩阵中只有零星的一些非零元素,其余的都是零元素,那么我们称之为稀疏矩阵,

经典算法题每日演练——第十七题 Dijkstra算法

原文:经典算法题每日演练--第十七题 Dijkstra算法 或许在生活中,经常会碰到针对某一个问题,在众多的限制条件下,如何去寻找一个最优解?可能大家想到了很多诸如“线性规划”,“动态规划” 这些经典策略,当然有的问题我们可以用贪心来寻求整体最优解,在图论中一个典型的贪心法求最优解的例子就莫过于“最短路径”的问题. 一:概序 从下图中我要寻找V0到V3的最短路径,你会发现通往他们的两点路径有很多:V0->V4->V3,V0->V1->V3,当然你会认为前者是你要找的最短 路径,那如

经典算法题每日演练——第七题 KMP算法

原文:经典算法题每日演练--第七题 KMP算法 在大学的时候,应该在数据结构里面都看过kmp算法吧,不知道有多少老师对该算法是一笔带过的,至少我们以前是的, 确实kmp算法还是有点饶人的,如果说红黑树是变态级的,那么kmp算法比红黑树还要变态,很抱歉,每次打kmp的时候,输 入法总是提示“看毛片”三个字,嘿嘿,就叫“看毛片算法”吧. 一:BF算法 如果让你写字符串的模式匹配,你可能会很快的写出朴素的bf算法,至少问题是解决了,我想大家很清楚的知道它的时间复 杂度为O(MN),原因很简单,主串和模

经典算法题每日演练——第二十一题 十字链表

原文:经典算法题每日演练--第二十一题 十字链表 上一篇我们看了矩阵的顺序存储,这篇我们再看看一种链式存储方法“十字链表”,当然目的都是一样,压缩空间. 一:概念 既然要用链表节点来模拟矩阵中的非零元素,肯定需要如下5个元素(row,col,val,down,right),其中: row:矩阵中的行. col:矩阵中的列. val:矩阵中的值. right:指向右侧的一个非零元素. down:指向下侧的一个非零元素. 现在我们知道单个节点该如何表示了,那么矩阵中同行的非零元素的表示不就是一个单链

经典算法题每日演练——第六题 协同推荐SlopeOne 算法

原文:经典算法题每日演练--第六题 协同推荐SlopeOne 算法 相信大家对如下的Category都很熟悉,很多网站都有类似如下的功能,“商品推荐”,"猜你喜欢“,在实体店中我们有导购来为我们服务,在网络上 我们需要同样的一种替代物,如果简简单单的在数据库里面去捞,去比较,几乎是完成不了的,这时我们就需要一种协同推荐算法,来高效的推荐浏览者喜 欢的商品. 一:概念 SlopeOne的思想很简单,就是用均值化的思想来掩盖个体的打分差异,举个例子说明一下: 在这个图中,系统该如何计算“王五“对”电

经典算法题每日演练——第十三题 赫夫曼树

原文:经典算法题每日演练--第十三题 赫夫曼树 赫夫曼树又称最优二叉树,也就是带权路径最短的树,对于赫夫曼树,我想大家对它是非常的熟悉,也知道它的应用场景, 但是有没有自己亲手写过,这个我就不清楚了,不管以前写没写,这一篇我们来玩一把. 一:概念 赫夫曼树里面有几个概念,也是非常简单的,先来看下面的图: 1. 基础概念 <1>  节点的权: 节点中红色部分就是权,在实际应用中,我们用“字符”出现的次数作为权. <2>  路径长度:可以理解成该节点到根节点的层数,比如:“A”到根节点