算法学习笔记——最长公共子序列(如何打印所有解?

本意还是想为百度搜索结果贡献出一份自己的力量,所以自上次“配环境”的开篇之作之后,小方小朋友决定继续记录一下自己的算法学习历程。本文主要介绍如何打印出全部的最长公共子序列,即最长公共子序列的所有解。

毕竟我懒得画图,所以别人造过的轮子,我是不会再造的。

关于最长公共子序列问题及其空间复杂度的各种优化方法,以下几篇文章已经说得很好了:

动态规划 最长公共子序列 过程图解(一般方法,图解很不错!!!)

最长公共子序列(LCS)动态规划的算法优化(一般方法+各种优化)

以下内容默认大家已经深刻理解了上述两篇文章

基于动态规划方法寻找最长公共子序列的算法的思路原本是用一个数组来记录所有子问题的最长公共子序列的长度,再通过一个回溯,去找到它的解,数组的示意图如下

思路是这样的:当i、j都不为0时,说明没有到起点,则:

1)若串x中i元素等于串y中j元素,说明这个元素就是公共子序列中的一个元素,而且说明这个数组中这个点的值来源于左上角的子问题的最长公共子序列长度再+1。为了正序输出,先递归调用print_lcs(i-1,j-1),再输出(执行完递归的程序在输出,保证先输出的是按顺序在前面的元素)

2)若不等,则由于我们在填表的时候,填的是左边和上边的最大值,所以回到数组中值较大的那一格。由于有相等的情况,所以当length[i-1][j]>=length[i][j-1]的时候回到(i-1,j)的子问题,即相等的时候都选上面的格子。

代码如下:

//打印最长公共子序列的解 
void LCS::print_lcs(int i, int j)
{
	//i=0或j=0->到终点了,结束 
        if(i==0 || j==0)
            return;
	//相等则输出
	if (x[i-1]==y[j-1]){
		print_lcs(i-1,j-1); 
		cout<<x[i-1]; 
	}
	else if(length[i-1][j]>=length[i][j-1]) 
		print_lcs(i-1,j);
	else 
		print_lcs(i,j-1);  
}

很容易看出,基于这种思路下的寻解方法,在存储最长公共子序列长度的数组中左格和上格相等时实际上都有两个分支(如上图(6,3)等格),即会对应两个不同的解(大概率殊途不同归,除非某个字符串中存在一样的元素,比如两个x,两个y之类的,但这也是两个解,具体没推过)。但我们在寻解时实际上在遇到这种情况的时候是通过选择同一个分支(比如都选左边或者都选上面),来找到其中一个解,并不能输出所有的解。

要解决这个问题,我们很自然地就会想到:既然只能输出一个解的根源在于回溯寻解时我们只选择了一个分支,那么,只要把另一个分支也考虑进来,不就行了?

那么,要怎样把两路都考虑进来呢?暴力寻解其实应该不太好写,而且我也没有想出来能怎么暴力

但首先,如下所示的三分支结构要变为4分支,即分支1:x[i-1]==y[j-1];分支2:length[i-1][j]>length[i][j-1];分支3:length[i][j-1]>length[i-1][j],分支4:length[i][j-1]==length[i-1][j](用else代替)

        //相等则输出
	if (x[i-1]==y[j-1]){
		print_lcs(i-1,j-1); 
		cout<<x[i-1]; 
	}
	else if(length[i-1][j]>=length[i][j-1]) 
		print_lcs(i-1,j);
	else 
		print_lcs(i,j-1);  

修改后: 

        //相等则输出
	if (x[i-1]==y[j-1]){
		print_lcs(i-1,j-1); 
		cout<<x[i-1]; 
	}
	else if(length[i-1][j]>=length[i][j-1]) 
		print_lcs(i-1,j);
	else if(length[i][j-1]>length[i-1][j]) 
		print_all(i,j-1);	
        else {
		print_all(i-1,j);
		print_all(i,j-1);
	}

但这还是有问题,如果直接这么执行的话,也就是说在length[i][j-1]==length[i-1][j]的时候,我们其实是想把(i-1,j)和(i,j-1)这个分支对应的不同解都打印出来,但是实际上这种写法会使得得到的最终结果是:先执行完得print_all(i-1,j),再执行print_all(i,j-1),也就是说两个子问题的解是连着打印的,其实得到的并不是我们想要的内容。

如何得到我们想要的东西呢?我们可以用一个数组,去临时存储解,当调用print_all(i-1,j)时,会更新子问题的解数组中对应元素的值,且增加一个current_len参数,用于记录指向解数组中空间的序号,一旦current_len跟实际的解的长度actual_len长度相等,就输出;也就不用担心数组中的值被覆盖的情况(因为x[i-1]==y[j-1]的时候,返回的参数current_len也会加1)。递归直到到最开始的递归函数中i或j==0了,才真正结束了。

也就相当于大家共一个起点,然后有分支的话,就先按着一个方向走到终点,然后输出一个序列,再退回到分支点,更新子问题中相等元素的值,走到终点了,又输出一个,直到所有的路都找完了,最开始的递归函数终于能执行到i或者j等于0的时候了,这个时候解就全部找到了,寻解过程也结束了。

由于仍然是一个反向寻解的过程,所以数组最后要逆序输出

void LCS::print_all(int i, int j,int current_len)
{
	//记录实际长度 
	int actual_len=length[length_x][length_y];
	//输出结果 
	if(current_len==actual_len)
	{
		for(int k=actual_len-1;k>=0;k--)
			cout<<lcs[k];
		cout<<endl;
	}
	//i=0或j=0->到终点了,结束 
        if(i==0 || j==0)
    	    return;
	else{
                //相等则输出
		if (x[i-1]==y[j-1]){
			lcs[current_len]=x[i-1];
			print_all(i-1,j-1,current_len+1);
		}
		else if(length[i-1][j]>length[i][j-1]) 
			print_all(i-1,j,current_len);
		else if(length[i][j-1]>length[i-1][j]) 
			print_all(i,j-1,current_len);		
		else {
			print_all(i-1,j,current_len);
			print_all(i,j-1,current_len);
		}
	}
	
}

下面贴上源代码,注释应该是比较详细了,有问题可以评论区问一下,毕竟,还可以涨一下积分hhhhhh

.c文件:

#define normal 0
#define least 0
#define improve 1

#include "lcs_normal.h"
#include "lcs_improve.h"
#include "lcs_least.h"

#define input 0

void input_string(LCS &solution);

int main()
{
	LCS solving_lcs;
	
#if input
	input_string(solving_lcs);
#else
    solving_lcs.x="xzyzzyx";
    solving_lcs.y="zxyyzxz";
#endif

    //求解 
    solving_lcs.lcsLength();
}

void input_string(LCS &solution){
	printf("输入第一个字符串:\n");
	getline(cin,solution.x);
	printf("输入第二个字符串:\n");
	getline(cin,solution.y);
	return;
}

其实有很多类似的头文件通过条件编译进行选择,但是其实.h里的东西都差不多,这里就贴一个吧(不要吐槽我把函数的定义写在.h里面,我平时也不这么写代码,真的!!!!!)

#ifndef LCS_IMPROVE_H_
#define LCS_IMPROVE_H_

#if improve

#include <iostream>
#include <string>
#include <vector>

using namespace std;

class LCS{
	public:
	string x,y;
	int length_x;
	int length_y;
	vector< vector<int> >length;
	void lcsLength();
	private:
	vector< char > lcs;//用于存其中一组解 
	void print_lcs(int i, int j);
	void print_all(int i, int j,int current_len);
};
 
/*入口参数:两个数组,一个用于存储长度的数组,一个用于存储移动路径的数组 
*更新:已定义在类中 
*出口:无 
*/
void LCS::lcsLength()
{
	//初始化长度 
    length_x=x.size();
    length_y=y.size();
    //初始化数组长度 
    //定义临时一维数组来同时指定两个维度的大小 
    vector<int> temp_int(length_y+1);
    //定义数组大小 
    length.resize(length_x+1,temp_int);
    //释放临时一维数组的空间 
	vector<int>().swap(temp_int);  
	
	//正式开始找啦!!! 
    for(int i=1;i<=length_x;i++){
        for(int j=1;j<=length_y;j++){
        	//若两元素相等,则为length[i-1][j-1]+1(去掉最后一个元素后的子问题的解:length[i-1][j-1],再加1) 
            if(x[i-1]==y[j-1]){
                length[i][j]=length[i-1][j-1]+1;
            }
            //不等的话,取大者 
			else if(length[i-1][j]>=length[i][j-1]){
                length[i][j]=length[i-1][j];
            }else{
                length[i][j]=length[i][j-1];
            }
        }
    }
	lcs.resize(length[length_x][length_y]);	
    //打印解
	//print_lcs(length_x,length_y); 
	//cout<<endl;
	print_all(length_x,length_y,0);
}

//打印最长公共子序列的解 
void LCS::print_lcs(int i, int j)
{
	//i=0或j=0->到终点了,结束 
    if(i==0 || j==0)
        return;
	//相等则输出
	if (x[i-1]==y[j-1]){
		print_lcs(i-1,j-1); 
		cout<<x[i-1]; 
	}
	else if(length[i-1][j]>=length[i][j-1]) 
		print_lcs(i-1,j);
	else 
		print_lcs(i,j-1);  
}

//打印所有最长公共子序列的解 
//入口参数:两个字符串的长度,和lcs中已存字符的长度 
void LCS::print_all(int i, int j,int current_len)
{
	//记录实际长度 
	int actual_len=length[length_x][length_y];
	//输出结果 
	if(current_len==actual_len)
	{
		for(int k=actual_len-1;k>=0;k--)
			cout<<lcs[k];
		cout<<endl;
	}
	//i=0或j=0->到终点了,结束 
    if(i==0 || j==0)
    	return;
	else{
		//相等则输出
		if (x[i-1]==y[j-1]){
			lcs[current_len]=x[i-1];
			print_all(i-1,j-1,current_len+1);
		}
		else if(length[i-1][j]>length[i][j-1]) 
			print_all(i-1,j,current_len);
		else if(length[i][j-1]>length[i-1][j]) 
			print_all(i,j-1,current_len);		
		else {
			print_all(i-1,j,current_len);
			print_all(i,j-1,current_len);
		}
	}
	
}

#endif

#endif

 分享到此结束,希望能对大家有帮助,谢谢!!!

完整代码大家可以自己去下载哦(手动咧嘴笑)

有问题欢迎评论区交流,欢迎批评指正!!!!

 

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值