最长公共子串问题(方法一:暴力+RK匹配,方法二:DP+空间优化)

时间:2014.09.05

地点:基地二楼

一、题目

给定一个query和一个text,均由小写字母组成。要求在text中找出以同样的顺序连续出现在query中的最长连续字母序列的长度。例如, query为“acbac”,text为“acaccbabb”,那么text中的“cba”为最长的连续出现在query中的字母序列,因此,返回结果应该为其长度3。

二、分析

对于该问题最直接的想法就是对query字符串的所有非空子字符串再text中进行查找比对,看是否也被包含在text中,然后取最长的那个的长度,即为所求。假设query字符串长度为n,那么query的子字符串就有C(n,2)个,即n(n-1)/2,然后这些子字符串还要在text中进行搜索匹配,朴素的搜索匹配算法复杂度是O(n^2),如此一来,该算法的时间复杂度为O(n^4),当然可以做一些优化,提高算法效率,比如对于匹配算法也还可以进行优化,比如RK算法,有限自动机和KMP算法等。

下面给出结合RK匹配的一个暴力算法代码:

1.先是RK算法,有关RK算法的再往后写出

bool RabinKarpMatch(const string& T, const string& P)
{
	static const int d = 128;
	static const int q = 6999997;

	int n = T.length();
	int m = P.length();
	int h = 1;
	for (int i = 1; i < m; i++)
		h = (h*d) % q;                  //h=d^(m-1) mode q

	int p = 0, t = 0;
	for (int i = 0; i < m; ++i)             //processing
	{
		p = ((p*d) + P[i]) % q;
		t = ((t*d) + T[i]) % q;
	}

	for (int s = 0; s < n - m + 1; ++s)     //s=[0...n-m+1-1]
	{
		if (t== p)
		{
			int i = 0;
			for (i; i < m; ++i)
			{
				if (P[i] != T[s + i])
					break;
			}
			if (i == m)
				return true;
		}
		t = (d*(t - T[s] * h% q+q) + T[s + m]) % q;
	}
	return false;
}

2.然后是顶层实现

size_t GetLargestCommomSubLen(const string& text, const string& query)
{
	size_t query_len = query.length();
	size_t text_len = text.length();
	assert(text_len >= query_len);
	if (text.empty() || query.empty())
		return 0;
	size_t max_len = 0;

	for (size_t start = 0; start < query_len; ++start)
	{
		size_t size = query_len - start;
		for (size_t len = 1; len <= size; ++len)
		{
			if (RabinKarpMatch(text, query.substr(start, len)))
			{
				if (len>max_len)
					max_len = len;
			}
		}
	}
	return max_len;
}

三、动态规划

记得有个斐波拉契数列问题,巧妙的用到了DP,使得算法效率得到了极大的改善,依照那种思路,先找到一种数学关系,类似F(n)=F(n-1)+F(n-2)或C(n,k)=C(n-1,k)+C(n-1,k-1),然后自底向上地去处理得到结果。在最长公共子字符串中这个问题表现为:

设text[ i ]和query[ j ]分别为上次匹配结尾的字符,在斐波拉契数列中,即是充分利用子问题的解去构造复杂问题的解,这里也一样,可用L[ i,j ]记录它们当前的匹配长度,于是在计算L[i+1,j+1]时,即匹配下一个字符时,只要看text[i+1]和query[j+1]还继不继续相等,相等则L[ i,j ]=L[ i-1,j-1 ] + 1,不相等则为0,结束匹配。再回到斐波拉契数列问题,在那里,用到自底向上的求解方式,首先是已知底为F(0)=0和F(1)=1,在这里,虽然不能很明显的知道底,然可通过计算得知,这个底即边界处理表现为:对与L第0行的处理,即text[0]与query各个字符的匹配情况。2.对L第0列的处理,即对于query[0],text各个字符的匹配情况。这样算法的时间复杂度降至O(n^2)

实现如下:

int GetLongestCommSubstrLen(const string& text, const string& query)
{
	int text_len = text.length();
	int query_len = query.length();
	if (text_len == 0 || 0 == query_len)
		return 0;
	vector<vector<int>> L(text_len, vector<int>(query_len, 0));
	int text_start = -1;
	int query_start = -1;

	for (int j = 0; j < query_len; ++j)
	{
		L[0][j] = (text[0] == query[j] ? 1 : 0);
	}

	for (int i = 1; i < text_len; ++i)
	{
		L[i][0] = (text[i] == query[0] ? 1 : 0);
		for (int j = 1; j < query_len; ++j)
		{
			if (text[i] == query[j])
			{
				L[i][j] = L[i - 1][j - 1] + 1;
			}
		}
	}

	int longest = 0;
	for (int i = 0; i < text_len; ++i)
	{
		for (int j = 0; j < query_len; ++j)
		{
			if (longest < L[i][j])
			{
				longest = L[i][j];
				text_start = i - longest + 1;
				query_start = j - longest + 1;
			}
		}
	}
	return longest;
}

这种方法采取了空间换时间的策略,尽管如此,在空间上,还可以优化,在空间的使用上并没有想象的那么恐怖。比如在计算斐波拉契数列时,其实求后一项只与前面两项相关,多余的信息存储造成了空间上的浪费,在这里同样也是如此,看公式L[ i,j ]=L[ i-1,j-1 ] + 1,亦知L的计算也只与前一行相关,而前一行的值是通过计算已知的了,于是只要两行存储空间即可,每当计算新的一行的,把旧行上升到第0行即可,swap一下即可。

改进后的代码如下(也非常简洁):

int GetLongestCommSubstrLen(const string& text, const string& query)
{
	int text_len = text.length();
	int query_len = query.length();
	if (text_len == 0 || 0 == query_len)
		return 0;
	vector<vector<int>> L(2, vector<int>(query_len, 0));
	int text_start = -1;
	int query_start = -1;

	int longest = 0;
	for (int j = 0; j < query_len; ++j)
	{
		if (text[0] == query[j])
		{
			L[0][j] = 1;
		}
	}

	for (int i = 1; i < text_len; ++i)
	{
		L[1][0] = (text[i] == query[0] ? 1 : 0);
		for (int j = 1; j < query_len; ++j)
		{
			if (text[i] == query[j])
			{
				L[1][j] = L[0][j - 1] + 1;
				if (longest < L[1][j])
					longest = L[1][j];
			}
		}
		L[1].swap(L[0]);
	}
	return longest;
}
时间: 2024-07-29 04:10:44

最长公共子串问题(方法一:暴力+RK匹配,方法二:DP+空间优化)的相关文章

Python练习题4(列表去重):[5,3,4,&#39;ok&#39;,4,3,&#39;abc&#39;,8,52,&#39;ok&#39;]去除列表中重复内容 方法一:使用set 方法二:不使用set,自己写方法

方法一:利用集合去重 1 list1 = [5,3,4,'ok',4,3,'abc',8,52,'ok'] 2 list1=list(set(list1)) 3 print(list1) 方法二:此方法略微冗余,先判断元素是否重复,再将重复元素提取并保存到新列表中,再for 新建的列表元素,删除原列表 1 def list_dup(ls): 2 list2 = [] 3 length = len(ls) #获取列表元素个数 4 for i in range(0,length-1): 5 for

一天一道算法题(5)---最长公共子串

题目 给定两个字符串str1和str2,返回两个字符串的最长公共子串.例如:str1="1AB2345CD",str2="12345EF",公共子串是"2345" 解析 最长公共子串和最长公共子序列的区别是,子串是连续的,子序列是不连续的. 首先还是要生成动态规划表.生成大小为M*N的矩阵dp.dp[i][j]的含义是,在必须把str1[i]和str2[j]当作公共子串最后一个字符的情况下,公共子串最长能有多长.比如,str1="A12

最长公共子串_暴力解法(不会正解)

最长公共子串 lcs.cpp/.in/.out 计算两个字符串的最大公共子串(Longest Common Substring)的长度,字符不区分大小写,这里的最大公共子串要求的字串是连续的. 输入: 两行,每行一个字符串,字符串长度<=1000. 输出: 输出一个整数,表示公共子串的长度. 样例输入: asdfas werasdfaswer 样例输出: 6 下面上代码: 1 #include<bits/stdc++.h> 2 using namespace std; 3 char o[

最长公共子序列|最长公共子串|最长重复子串|最长不重复子串|最长回文子串|最长递增子序列|最大子数组和

参考:http://www.ahathinking.com/archives/124.html 最长公共子序列 1.动态规划解决过程 1)描述一个最长公共子序列 如果序列比较短,可以采用蛮力法枚举出X的所有子序列,然后检查是否是Y的子序列,并记录所发现的最长子序列.如果序列比较长,这种方法需要指数级时间,不切实际. LCS的最优子结构定理:设X={x1,x2,……,xm}和Y={y1,y2,……,yn}为两个序列,并设Z={z1.z2.……,zk}为X和Y的任意一个LCS,则: (1)如果xm=

POJ 2774 最长公共子串

对于最长公共子串,n*m的递推显然无法通过本题. 本题是后缀数组的一个基础应用,字符串的子串可以视作后缀的前缀. 我们在两个串间插入一个不在字符集的字符如'#'作为连接,这样做的目的是为了防止两个后缀的最长公共前缀跨过第一个字符串的末尾. 扫描Height数组,如果排名为i的字符串与排名为i-1的字符串来源于原来的2个串,则更新答案最大值. 1 #include <iostream> 2 #include <vector> 3 #include <algorithm>

动态规划之最长公共子串

一 问题引入 在生物学中,经常需要比较两个不同生物的DNA,一个DNA串由由一串称为碱基的的分子组成,碱基有鸟嘌呤,腺嘌呤,胞嘧啶,胸腺嘧啶四中,我们用英文字母的首字母表示四种碱基,那么DNA就是在有限集{A,C,G,T}上的一个字符串.例如某种生物的DNA序列为:S1=ACCGGTCGAGTGCGCGGAAGCCGGCCGAA,S2=GTCGTTCGGAATGCCGTTGCTCTGTAAA,我们比较两个DNA串的原因就是希望确定他们的相似度,作为衡量两个物种相似度的标准.如果一个串是另外一个串

lintcode 中等题:longest common substring 最长公共子串

题目 最长公共子串 给出两个字符串,找到最长公共子串,并返回其长度. 样例 给出A=“ABCD”,B=“CBCE”,返回 2 注意 子串的字符应该连续的出现在原字符串中,这与子序列有所不同. 解题 注意: 子序列:这个序列不是在原字符串中连续的位置,而是有间隔的,如:ABCDE  和AMBMCMDMEM 最长公共子序列是ADCDE 子串:子串一定在原来字符串中连续存在的.如:ABCDEF 和SSSABCDOOOO最长公共子串是ABCD 参考链接,讲解很详细 根据子串定义,暴力破解 public

利用后缀数组(suffix array)求最长公共子串(longest common substring)

摘要:本文讨论了最长公共子串的的相关算法的时间复杂度,然后在后缀数组的基础上提出了一个时间复杂度为o(n^2*logn),空间复杂度为o(n)的算法.该算法虽然不及动态规划和后缀树算法的复杂度低,但其重要的优势在于可以编码简单,代码易于理解,适合快速实现. 首先,来说明一下,LCS通常指的是公共最长子序列(Longest Common Subsequence,名称来源参见<算法导论>原书第3版p223),而不是公共最长子串(也称为最长公共子串). 最长公共子串问题是在文本串.模式串中寻找共有的

BZOJ 2946 POI2000 公共串 后缀自动机(多串最长公共子串)

题意概述:给出N个字符串,每个串的长度<=2000(雾...可能是当年的年代太久远机子太差了),问这N个字符串的最长公共子串长度为多少.(N<=5) 抛开数据结构,先想想朴素做法. 设计一种稳定的暴力算法.可以想到这样一种做法:首先确定一个串,枚举每个位置,然后暴力计算其他每个串以这个位置开头的最长匹配,取最小值,就是在公共子串在我们确定下来的串的这个位置开头的时候所能得到的最长公共子串.不难发现把这个问题转化成后缀的形式也是一样的.同时发现可能在枚举多个位置的时候答案甚至最后构造出来的串都是

[PHP]算法-最长公共子串的PHP实现

最长公共子串问题: 给定两个字符串,求出它们之间最长的相同子字符串的长度. 暴力解法思路: 1.以两个字符串的每个字符为开头,往后比较,这样就会需要两层循环 2.两层循环内部的比较方式,也是一层循环,以当前字符为起点,往后遍历比较,直到有不同就跳出这次循环,记录下相同子字符串的长度 3.以最长的那次长度为准,因此也就是有三层循环.时间复杂度O(n^3) longest=0 for i=0;i<str1.size;i++ for j=0;j<str2.size;j++ m=i n=j lengt