您的位置:首页 > 其它

算法系列笔记6(动态规划—最长公共子序列/串lcs和最长递增子序列(LIS))

2015-02-06 10:00 591 查看
子序列要求元素顺序一致就可以了,而字串必须是连续的。如ABCBDAB与BDCABA两个字符串,最长公共子序列有BCBA、BDAB和BCAB, 而最长公共字串只有AB和BD<连续>。当然这里的求解只求一个,但通常是这样直接说求最长公共子串,子序列,准确的应该是之一。

最长公共子序列

法一:穷举法

检查字符串x所有字序列,共有2^m个,检查它是否在y字符串中出现,每个需要O(n),时间复杂度为指数级的。

法二:动态规划(DP)

将两个字符串x[1…m]和y[1…n]放在x轴和y轴方向上便得到一个二维数组c[i,j]来记录x[1…i]和y[1…j]最长公共子序列个数。

当x[i]==y[j]的时候 c[i,j] = c[i-1,j-1]+1;当不相等的时候,c[i,j] = max{c[i-1, j], c[i,j-1]}.

采用自底向上的思想,这样时间复杂度就等于lcs独立子问题的个数O(mn),不然需要重复计算子问题,时间复杂度仍然为指数级的。

代码如下:



// 最长公共子序列(不连续)   需要有个标记数组用于回溯
void lcs_sequences(const char* str1, const char* str2, int len1, int len2)
{
	int **c = new int*[len1+1];
	int **b = new int*[len1+1];
	int i, j;
	for(i = 0; i < len1+1; i++)
	{
		c[i] = new int[len2+1];
		b[i] = new int[len2+1];
	}
	for(i = 0; i <= len1; i ++)
		for(j = 0; j <= len2; j++)
			c[i][j] = 0;
	for(i = 1; i <= len1; i++)
	{
		for(j = 1; j <= len2; j++)
		{
			if(str1[i-1] == str2[j-1])
			{
				c[i][j] = c[i-1][j-1] +1;
				b[i][j] = 0;  // 来自左上
			}
			else{
				if(c[i-1][j] > c[i][j-1]){
					c[i][j] = c[i-1][j];
					b[i][j] = 1;    // 来自上方
				}
				else{
					c[i][j] = c[i][j-1];   // 来自左方
					b[i][j] = 2;    // 来自上方
				}
			}
		}
	}
	cout << "最长公共子序列长度: " << c[len1][len2] << endl;

	// 回溯求解路径
	i = len1;
	j = len2;
	char *x = new char[c[len1][len2]];
	int k = 0;
	/*while(i > 0 && j > 0){
		if(b[i][j] == 0)   // 来自左上
		{
			x[k++] = str1[i-1];
			//cout << str1[i-1];
			i--;
			j--;
		}
		else if(b[i][j] == 1) i--;
		else j--;
	}*/

	// 不使用标记数组进行回溯  直接使用str1和str2及c[i][j]得出结果
	while(i > 0 && j > 0)
	{
		if(str1[i-1] == str2[j-1])
		{
			x[k++] = str1[i-1];
			i--;
			j--;
		}
		else if(c[i][j] == c[i][j-1]) j--;
		else i--;
	}

	cout << "the lcs_opt is: " ;
	for(i = c[len1][len2]-1; i >= 0 ; i--)
	{
		cout << x[i];
	}
	cout << endl;
	for(i= 0; i < len1; i++)
		delete[] c[i];
	delete []c;
	delete []x;
}


上面注释的代码,我们用标记数组来跟踪来源,当然也可以不适用标记数字,直接使用c[i,j]与str1、str2来判断,这里就可以节省O(mn)的空间,但是只是在空间复杂性的常数因子上的改进。

如果只要求公共字串的长度而不求字串是什么,则空间复杂度可以降低到O(min{m,n}).这里用到了二维数组,但是行数固定为2.

代码如下:



// 最长公共子序列优化空间,用两行的二维数组, 但此时只能得到最长公共子序列的长度, 无法得到路径
void swap(int **c, int len2)
{
	for(int i = 0; i < len2; i++)
	{
		int temp = c[0][i];
		c[0][i] = c[1][i];
		c[1][i] = temp;
	}
}

void lcs_sequences_opt(const char* str1, const char* str2, int len1, int len2)
{
	int *c[2];
	int i,j;
	for(i = 0; i < 2; i++)
		c[i] = new int[len2];
	for(j = 0; j < len2; j++)
		c[0][j] = 0;
	for(i = 0; i < len1; i++)
	{
		for(j = 0; j < len2; j++)
		{
			if(str1[i] == str2[j])
			{
				if(j == 0) c[1][j] = 1;
				else c[1][j] = c[0][j-1] + 1;
			}
			else
			{
				if(j == 0) c[1][j] = c[0][j];
				else c[1][j] = c[0][j] > c[1][j-1] ? c[0][j]: c[1][j-1];
			}
		}
		swap(c, len2);           //  不用交换 直接将c[1]赋值给c[0]就可以了
	}
	cout << "最长公共子序列长度为: " << c[0][len2-1] << endl; 

	for(i = 0; i < 2; i++)
		delete[] c[i];
}


最长公共子串

解法就是用一个矩阵来记录两个字符串中所有位置的两个字符之间的匹配情况,若是匹配则为1,否则为0。然后求出对角线最长的1序列,其对应的位置就是最长匹配子串的位置.

优化:当字符匹配的时候,我们并不是简单的给相应元素赋上1,而是赋上其左上角元素的值加一。我们用两个标记变量来标记矩阵中值最大的元素的位置,在矩阵生成的过程中来判断当前生成的元素的值是不是最大的,据此来改变标记变量的值,那么到矩阵完成的时候,最长匹配子串的位置和长度就已经出来了。

即:

当x[i]==y[j]的时候 c[i,j] = c[i-1,j-1]+1;当不相等的时候,c[i,j] = 0.

代码如下:



// 求最长公共子串
void lcs_string(const char*str1, const char *str2, int len1, int len2)
{
	int **c = new int*[len1];
	int i, j;
	int maxC = 0;    // 最大值
	int position = 0;   // 位置
	for(i = 0; i < len1; i++)
	{
		c[i] = new int[len2];
		for(j = 0; j < len2; j++)
		{
			if(str1[i] == str2[j])
			{
				if(i == 0 || j == 0)
				{
					c[i][j] = 1;
				}
				else{
					c[i][j] = c[i-1][j-1] + 1;
				}
			}
			else{
				c[i][j] = 0;
			}
			if(c[i][j] > maxC)
			{
				maxC = c[i][j];
				position = j;
			}
		}	
	}
	cout << "最大公共子串长度为: " << maxC << endl;
	cout << "the lcs is: " ;
	for(i = position - maxC+1; i <= position; i++)
	{
		cout << str2[i];
	}
	cout << endl;

	for(i = 0; i < len1; i++)
		delete[] c[i];
	delete []c;	
}


此时时间复杂度和空间复杂度都为O(mn)。当然也可以对代码的空间复杂度进行优化到O(min{m,n}),这里只需要用一维数组就可以了,但是从后到前进行遍历,这样c[j]前面的才是上一次的结果,否则如DBB与AB就会出错。

代码如下:

// 求最长公共子串 优化空间复杂度  用一维数组就可以搞定
void lcs_string_opt(const char*str1, const char *str2, int len1, int len2)
{
	int *c = new int[len2];
	int i, j;
	int maxC = 0;    // 最大值
	int position = 0;   // 位置
	memset(c, 0, sizeof(int)*len2);
	for(i = 0; i < len1; i++)
	{
		for(j = len2-1; j >= 0; j--)   // 从后到前进行遍历 这样c[j]前面的才是上一次的结果 否则如DBB与AB就会出错
		{
			if(str1[i] == str2[j])
			{
				if(j == 0) c[j] = 1;
				else c[j] = c[j-1]+1;       // 唯一的区别 
			}
			else c[j] = 0;
			if(c[j] > maxC)           
			{
				maxC = c[j];
				position = j;
			}
		}
	}
	cout << "最大公共子串长度为: " << maxC << endl;
	cout << "the lcs_opt is: " ;
	for(i = position - maxC+1; i <= position; i++)        // 输出最长公共字串
	{
		cout << str2[i];
	}
	cout << endl;

	delete []c;
}


动态规划

动态规划具有两大特性,我们通过最长公共子序列来作为例子。

1:最优子结构

意思是问题的最优解包含了子问题的最优解。

如lcs中求x[1…i]和y[1…j]的最长公共子序列,当x[i]==y[j]时,可以转换为求x[1…i-1]和y[1…j-1]的最长公共子序列。当x[i]不等于y[j],需要计算出x[1…i]和y[1…j-1]及x[1…i-1]和y[1…j]的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算x[1…i-1]和y[1…j-1]的最长公共子序列。

2:重叠子问题

我们也看到子问题都包含一个公共子问题,即会出现重叠问题。

意思就是指一个递归问题包含少数独立的子问题被反复计算了多次。lcs问题中就包含了m*n个独立的子问题。

动态规划算法的步骤:



最长递增子序列(LIS, longest increasedsequence)

比如:数组a=[5,6,7,8,12, 1,2, 9,10]的最长递增子序列为[5,6,7,8,9,10]。长度为6,这里给出三种方法。(前两种方法的时间复杂度为O(N^2),后面一种方法的时间复杂度为O(NlgN))

法一:最长公共子序列

求最长递增子序列可以转换为求原数组和其有序数组的最长公共子序列,得到的结果也一定是单调递增的。时间复杂度为O(N^2),这里只给出了求子序列长度的代码,如下求出子序列是什么需要保存一个标记数组或者通过递归算法实现.

代码如下:



// 通过与原有序序列 求最长公共子序列求解
int lcsAlg(int *a, const int &len){
	int *b = new int[len];
	int **dp = new int*[len+1];
	for(int i = 0; i <= len; i++){
		dp[i] = new int[len+1];
		memset(dp[i], 0, sizeof(int)*len);
	}
	for(int i = 0; i < len; i++)
		b[i] = a[i];
	sort(b, b+len);
	int maxLIS = 0;
	for(int i = 1; i <= len; i++)
	{
		for(int j = 1; j <= len; j++){
			if(b[i-1] == a[j-1])dp[i][j] = dp[i-1][j-1]+1;     // 递推迭代式
			else dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
			maxLIS = max(maxLIS, dp[i][j]);
		}
	}
	for(int i = 0; i <= len; i++)
		delete []dp[i];
	delete []dp;
	delete []b;
	return maxLIS;
}


法二:动态规划

我们用dp[i]用来表示下标为i的元素结尾最长递增子序列,那么会得到如下递归式:

dp[i] =max(dp[k]+1, dp[i]) if k = 0到i-1,且a[k]<a[i]。然后我们在dp[i]中最大的即为要求的最长递增子序列的长度。求递增子序列是什么同上。

这样时间复杂度也为O(N^2)

代码如下:

// 动态规划问题求解LIS(最长递增子数组),同理可以用递归或者设定一个数组记录前缀求解子序列
int dpAlg(int *a, const int &len){
	int *dp = new int[len];
	memset(dp, 0, sizeof(int)*len);
	int maxLIS = 0;
	for(int i = 0; i < len; i++){
		dp[i] = 1;
		for(int j = 0; j < i; j++){
			if(a[j] < a[i] && dp[i] < dp[j]+1){    //  dp[i] = dp[k]+1 其中k属于[0,i-1]
				dp[i] = dp[j]+1;
				maxLIS = max(maxLIS, dp[i]);    // 最大递增子序列长度
			}
		}
	}
	delete []dp;
	return maxLIS;
}


法三:DP+二分查找

该方法的时间复杂度为O(NlgN),但是它只能求出递增子序列的长度,无法求出递增子序列是什么,当然可以保存一个dp数组用来保存ans的值,这样就能求出递增子序列是什么了。

代码中用到一数组B, 其中B[i]表示LIS中长度为i的最小元素,当我们更新a[j],找到B中第一个比a[j]大的元素的下标,然后替换为a[j],网上一个例子解释如下:

例子:

假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。

下面一步一步试着找出它。

我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。

此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2]= 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2]= 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] =6, 这时B[1..3] = 1, 3,6,还是很容易理解吧? Len = 3了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3,4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] =7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3,4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

代码:

// 二分查找找到大于等于value的元素的下标
int binarySearch(int *B, int left, int right, int value){
	while(left <= right){
		int mid = (left+right)>>1;
		if(B[mid] < value) left = mid+1;
		else if(B[mid] == value) return mid;
		else right = mid-1;
	}
	return left;
}

// B[i]表示递增子序列长度为i对应的元素的最小值;;;但是对于求最长递增子序列是什么应该没有办法
int dpBinaryAlg(int *a, const int &len){
	int *B = new int[len+1];
	memset(B, 0, sizeof(int)*(len+1));
	int ans = 1;
	for(int i = 0; i < len; i++){
		if(ans == 1 || a[i] > B[ans-1]){   // 如果是第一个元素或者比递增子序列长度都要大
			B[ans++] = a[i];
		}else{                  // 不是最大的,更新
			int t = binarySearch(B, 1, ans-1, a[i]);
			B[t] = a[i];
		}
	}
	
	//for(int i = 1; i < ans; i++)
	//	cout << B[i] << " ";
	//cout << endl;
	delete []B;
	return ans-1;
}


主函数代码:

int main(){
	//int a[10] = {5, 6, 7, 8, 12, 1, 2, 3, 9, 10};
	int a[10] = {2, 1, 5, 3, 6, 4, 8, 9, 7};
	cout << dpAlg(a, 9) << endl;
	cout << lcsAlg(a, 9) << endl;
	cout << dpBinaryAlg(a, 9) << endl;
	return 0;
}


参考文献

1:http://blog.csdn.net/steven30832/article/details/8260189

2:http://blog.csdn.net/imzoer/article/details/8031478

3:http://www.ahathinking.com/archives/117.html 最长递增子序列(LIS)

4:http://www.felix021.com/blog/read.php?1587
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐