关于用动态规划法求最大公共子序列的问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于用动态规划法求最大公共子序列的问题相关的知识,希望对你有一定的参考价值。

这个代码始终理解不了,不知道是什么原理,能不能给个比较详细的讲解。讲解一下原理。每行代码给个注释..........真是有点抽象,脑子有点笨,转不过来。。。。
#include <iostream>
#include <string>
using namespace std;
#define N 100
char a[N], b[N], str[N];
int c[N][N];
int lcs_len(char* a, char* b,int c[][N])

int m=strlen(a), n=strlen(b), i, j;
for (i=0; i<=m; i++)
c[i][0]=0;
for (i=0; i<=n; i++)
c[0][i]=0;
for (i=1; i<=m; i++)

for (j=1; j<=n; j++)


if (a[i-1]==b[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];


return c[m][n];


char* build_lcs(char s[],char* a,char* b)

int i=strlen(a), j=strlen(b);
int k=lcs_len(a,b,c);
s[k]='\0';
while (k>0)

if (c[i][j]==c[i-1][j])
i--;
else if (c[i][j]==c[i][j-1])
j--;
else

s[--k]=a[i-1];
i--; j--;


cout<<s<<endl;
return s;

void main()

cout<<"输入两个长度小于"<<N<<"的字符串"<<endl;
cin>>a;
cin>>b;
cout<<"LCS="<<build_lcs(str,a,b)<<endl;

规划内容:

实验四:动态规划
实验目的:理解动态规划的基本思想,理解动态规划算法的两个基本要素最优子结构性质和子问题的重叠性质。熟练掌握典型的动态规划问题。掌握动态规划思想分析问题的一般方法,对较简单的问题能正确分析,设计出动态规划算法,并能快速编程实现。
实验内容:编程实现讲过的例题:最长公共子序列问题、矩阵连乘问题、凸多边形最优三角剖分问题、电路布线问题等。本实验中的问题,设计出算法并编程实现。
习题
1. 最长公共子序列
一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X=<x1, x2,…, xm>,则另一序列Z=<z1, z2,…, zk>是X的子序列是指存在一个严格递增的下标序列 <i1, i2,…, ik>,使得对于所有j=1,2,…,k有

解答如下:
a) 最长公共子序列的结构
若用穷举搜索法,耗时太长,算法需要指数时间。
易证最长公共子序列问题也有最优子结构性质
设序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的一个最长公共子序列Z=<z1, z2, …, zk>,则:
i. 若xm=yn,则zk=xm=yn且Zk-1是Xm-1和Yn-1的最长公共子序列;
ii. 若xm≠yn且zk≠xm ,则Z是Xm-1和Y的最长公共子序列;
iii. 若xm≠yn且zk≠yn ,则Z是X和Yn-1的最长公共子序列。
其中Xm-1=<x1, x2, …, xm-1>,Yn-1=<y1, y2, …, yn-1>,Zk-1=<z1, z2, …, zk-1>。
最长公共子序列问题具有最优子结构性质。
b) 子问题的递归结构
由最长公共子序列问题的最优子结构性质可知,要找出X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>的最长公共子序列,可按以下方式递归地进行:当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。
由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。
我们来建立子问题的最优值的递归关系。用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。建立递归关系如下:

c) 计算最优值
由于在所考虑的子问题空间中,总共只有θ(m*n)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率。
计算最长公共子序列长度的动态规划算法LCS_LENGTH(X,Y)以序列X=<x1, x2, …, xm>和Y=<y1, y2, …, yn>作为输入。输出两个数组c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存储Xi与Yj的最长公共子序列的长度,b[i,j]记录指示c[i,j]的值是由哪一个子问题的解达到的,这在构造最长公共子序列时要用到。最后,X和Y的最长公共子序列的长度记录于c[m,n]中。
程序如下:
#include<stdio.h>
#include<string.h>
int lcs_length(char x[], char y[]);
int main()

char x[100],y[100];
int len;
while(1)

scanf("%s%s",x,y);
if(x[0]=='0') //约定第一个字符串以‘0’开始表示结束
break;
len=lcs_length(x,y);
printf("%d\n",len);


int lcs_length(char x[], char y[] )

int m,n,i,j,l[100][100];
m=strlen(x);
n=strlen(y);
for(i=0;i<m+1;i++)
l[i][0]=0;
for(j=0;j<n+1;j++)
l[0][j]=0;
for(i=1;i<=m;i++)
for(j=1;j<=n;j++)
if(x[i-1]==y[j-1]) //i,j从1开始,但字符串是从0开始
l[i][j]=l[i-1][j-1]+1;
else if(l[i][j-1]>l[i-1][j])
l[i][j]=l[i][j-1];
else
l[i][j]=l[i-1][j];
return l[m][n];

由于每个数组单元的计算耗费Ο(1)时间,算法lcs_length耗时Ο(mn)。
思考:空间能节约吗?
2. 计算矩阵连乘积
在科学计算中经常要计算矩阵的乘积。矩阵A和B可乘的条件是矩阵A的列数等于矩阵B的行数。若A是一个p×q的矩阵,B是一个q×r的矩阵,则其乘积C=AB是一个p×r的矩阵。由该公式知计算C=AB总共需要pqr次的数乘。其标准计算公式为:

现在的问题是,给定n个矩阵。其中Ai与Ai+1是可乘的,i=1,2,…,n-1。要求计算出这n个矩阵的连乘积A1A2…An。
递归公式:
程序如下:
#include<stdio.h>
int main()

int p[101],i,j,k,r,t,n;
int m[101][101]; //为了跟讲解时保持一致数组从1开始
int s[101][101]; //记录从第i到第j个矩阵连乘的断开位置
scanf("%d",&n);
for(i=0;i<=n;i++)
scanf("%d",&p[i]); //读入p[i]的值(注意:p[0]到p[n]共n+1项)
for(i=1;i<=n;i++) //初始化m[i][i]=0
m[i][i]=0;
for(r=1;r<n;r++) //r为i、j相差的值
for(i=1;i<n;i++) //i为行

j=i+r; //j为列
m[i][j]=m[i+1][j]+p[i-1]*p[i]*p[j]; //给m[i][j]赋初值
s[i][j]=i;
for(k=i+1;k<j;k++)

t=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if(t<m[i][j])

m[i][j]=t; //m[i][j]取最小值
s[i][j]=k;



printf("%d",m[1][n]);

3. 凸多边形的最优三角剖分
多边形是平面上一条分段线性的闭曲线。也就是说,多边形是由一系列首尾相接的直线段组成的。组成多边形的各直线段称为该多边形的边。多边形相接两条边的连接点称为多边形的顶点。若多边形的边之间除了连接顶点外没有别的公共点,则称该多边形为简单多边形。一个简单多边形将平面分为3个部分:被包围在多边形内的所有点构成了多边形的内部;多边形本身构成多边形的边界;而平面上其余的点构成了多边形的外部。当一个简单多边形及其内部构成一个闭凸集时,称该简单多边形为凸多边形。也就是说凸多边形边界上或内部的任意两点所连成的直线段上所有的点均在该凸多边形的内部或边界上。
通常,用多边形顶点的逆时针序列来表示一个凸多边形,即P=<v0 ,v1 ,… ,vn-1>表示具有n条边v0v1,v1v2,… ,vn-1vn的一个凸多边形,其中,约定v0=vn 。
若vi与vj是多边形上不相邻的两个顶点,则线段vivj称为多边形的一条弦。弦 将多边形分割成凸的两个子多边形<vi ,vi+1 ,… ,vj>和<vj ,vj+1 ,… ,vi>。多边形的三角剖分是一个将多边形分割成互不重迭的三角形的弦的集合T。如图是一个凸多边形的两个不同的三角剖分。

凸多边形最优三角剖分的问题是:给定一个凸多边形P=<v0 ,v1 ,… ,vn-1>以及定义在由多边形的边和弦组成的三角形上的权函数ω。要求确定该凸多边形的一个三角剖分,使得该三角剖分对应的权即剖分中诸三角形上的权之和为最小。
可以定义三角形上各种各样的权函数W。例如:定义ω(△vivjvk)=|vivj|+|vivk|+|vkvj|,其中,|vivj|是点vi到vj的欧氏距离。相应于此权函数的最优三角剖分即为最小弦长三角剖分。(注意:解决此问题的算法必须适用于任意的权函数)
4. 防卫导弹
一种新型的防卫导弹可截击多个攻击导弹。它可以向前飞行,也可以用很快的速度向下飞行,可以毫无损伤地截击进攻导弹,但不可以向后或向上飞行。但有一个缺点,尽管它发射时可以达到任意高度,但它只能截击比它上次截击导弹时所处高度低或者高度相同的导弹。现对这种新型防卫导弹进行测试,在每一次测试中,发射一系列的测试导弹(这些导弹发射的间隔时间固定,飞行速度相同),该防卫导弹所能获得的信息包括各进攻导弹的高度,以及它们发射次序。现要求编一程序,求在每次测试中,该防卫导弹最多能截击的进攻导弹数量,一个导弹能被截击应满足下列两个条件之一:
a)它是该次测试中第一个被防卫导弹截击的导弹;
b)它是在上一次被截击导弹的发射后发射,且高度不大于上一次被截击导弹的高度的导弹。
输入数据:第一行是一个整数n,以后的n各有一个整数表示导弹的高度。
输出数据:截击导弹的最大数目。
分析:定义l[i]为选择截击第i个导弹,从这个导弹开始最多能截击的导弹数目。
由于选择了第i枚导弹,所以下一个要截击的导弹j的高度要小于等于它的高度,所以l[i]应该等于从i+1到n的每一个j,满足h[j]<=h[i]的j中l[j]的最大值。
程序如下:
#include<stdio.h>
int main()

int i,j,n,max,h[100],l[100];
scanf("%d",&n);
for(i=0;i<n;i++)
scanf("%d",&h[i]);
l[n-1]=1;
for(i=n-2;i>=0;i--)

max=0;
for(j=i+1;j<n;j++)
if(h[i]>h[j]&&max<l[j])
max=l[j];
l[i]=max+1;

printf("%d",l[0]);

5. 石子合并
在一个圆形操场的四周摆放着n堆石子(n<= 100),现要将石子有次序地合并成一堆。规定每次只能选取相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。编一程序,由文件读入堆栈数n及每堆栈的石子数(<=20)。
选择一种合并石子的方案,使得做n-1次合并,得分的总和最小;
输入数据:
第一行为石子堆数n;
第二行为每堆的石子数,每两个数之间用一个空格分隔。
输出数据:
从第一至第n行为得分最小的合并方案。第n+1行是空行.从第n+2行到第2n+1行是得分最大合并方案。每种合并方案用n行表示,其中第i行(1<=i<=n)表示第i次合并前各堆的石子数(依顺时针次序输出,哪一堆先输出均可)。要求将待合并的两堆石子数以相应的负数表示。
Sample Input
4
4 5 9 4
Sample Output
-4 5 9 -4
-8 -5 9
-13 -9
22 4 -5 -9 4
4 -14 -4
-4 -18
22

6. 最小代价子母树
设有一排数,共n个,例如:22 14 7 13 26 15 11。任意2个相邻的数可以进行归并,归并的代价为该两个数的和,经过不断的归并,最后归为一堆,而全部归并代价的和称为总代价,给出一种归并算法,使总代价为最小。
输入、输出数据格式与“石子合并”相同。
Sample Input
4
12 5 16 4
Sample Output
-12 -5 16 4
17 -16 -4
-17 -20
37
7. 商店购物
某商店中每种商品都有一个价格。例如,一朵花的价格是2 ICU(ICU 是信息学竞赛的货币的单位);一个花瓶的价格是5 ICU。为了吸引更多的顾客,商店提供了特殊优惠价。特殊优惠商品是把一种或几种商品分成一组。并降价销售。例如:3朵花的价格不是6而是5 ICU;2个花瓶加1朵花是10 ICU不是12 ICU。
编一个程序,计算某个顾客所购商品应付的费用。要充分利用优惠价以使顾客付款最小。请注意,你不能变更顾客所购商品的种类及数量,即使增加某些商品会使付款总数减小也不允许你作出任何变更。假定各种商品价格用优惠价如上所述,并且某顾客购买物品为:3朵花和2个花瓶。那么顾客应付款为14 ICU因为:
1朵花加2个花瓶优惠价:10 ICU
2朵花正常价:4 ICU
输入数据:第一个文件INPUT.TXT描述顾客所购物品(放在购物筐中);第二个文件描述商店提供的优惠商品及价格(文件名为OFF ER.TXT)。 两个文件中都只用整数。
第一个文件INPUT.TXT的格式为:第一行是一个数字B(0≤B≤5),表示所购商品种类数。下面共B行,每行中含3个数C,K,P。 C 代表商品的编码(每种商品有一个唯一的编码),1≤C≤999。K代表该种商品购买总数,1≤K≤5。P 是该种商品的正常单价(每件商品的价格),1≤P≤999。请注意,购物筐中最多可放5*5=25件商品。
第二个文件OFFER.TXT的格式为:第一行是一个数字S(0≤S≤9 9),表示共有S 种优惠。下面共S行,每一行描述一种优惠商品的组合中商品的种类。下面接着是几个数字对(C,K),其中C代表商品编码,1≤C≤9 99。K代表该种商品在此组合中的数量,1≤K≤5。本行最后一个数字P(1≤ P≤9999)代表此商品组合的优惠价。当然, 优惠价要低于该组合中商品正常价之总和。
输出数据:在输出文件OUTPUT.TXT中写 一个数字(占一行),该数字表示顾客所购商品(输入文件指明所购商品)应付的最低货款。
8. 旅游预算
一个旅行社需要估算乘汽车从某城市到另一城市的最小费用,沿路有若干加油站,每个加油站收费不一定相同。旅游预算有如下规则:
若油箱的油过半,不停车加油,除非油箱中的油不可支持到下一站;每次加油时都加满;在一个加油站加油时,司机要花费2元买东西吃;司机不必为其他意外情况而准备额外的油;汽车开出时在起点加满油箱;计算精确到分(1元=100分)。编写程序估计实际行驶在某路线所需的最小费用。
输入数据:从当前目录下的文本文件“route.dat”读入数据。按以下格式输入若干旅行路线的情况:
第一行为起点到终点的距离(实数)
第二行为三个实数,后跟一个整数,每两个数据间用一个空格隔开。其中第一个数为汽车油箱的容量(升),第二个数是每升汽油行驶的公里数,第三个数是在起点加满油箱的费用,第四个数是加油站的数量。(〈=50)。接下去的每行包括两个实数,每个数据之间用一个空格分隔,其中第一个数是该加油站离起点的距离,第二个数是该加油站每升汽油的价格(元/升)。加油站按它们与起点的距离升序排列。所有的输入都有一定有解。
输出数据:答案输出到当前目录下的文本文件“route.out”中。该文件包括两行。第一行为一个实数和一个整数,实数为旅行的最小费用,以元为单位,精确到分,整数表示途中加油的站的N。第二行是N个整数,表示N个加油的站的编号,按升序排列。数据间用一个空格分隔,此外没有多余的空格。
Sample Input
516.3 38.09 1
15.7 22.1 20.87 3 2
125.4 1.259
297.9 1.129
345.2 0.999
Sample Output
38.09 1
2
9. 皇宫看守
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状;某些宫殿间可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
请你编程计算帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
输入数据:输入数据由文件名为intput.txt的文本文件提供。输入文件中数据表示一棵树,描述如下:
第1行 n,表示树中结点的数目。
第2行至第n+1行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号i(0<i<=n),在该宫殿安置侍卫所需的经费k,该边的儿子数m,接下来m个数,分别是这个节点的m个儿子的标号r1,r2,...,rm。
对于一个n(0 < n <= 1500)个结点的树,结点标号在1到n之间,且标号不重复。
输出数据:输出到output.txt文件中。输出文件仅包含一个数,为所求的最少的经费。
如右图的输入数据示例:
Sample Input
6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0
Sample Output
25
10. 游戏室问题
有一个游戏室里有多个游戏室,并且这每个游戏室里还有多个游戏室,每个游戏室里面还有游戏室,依此类推。进入每个游戏室都可得到一定的快乐,每个游戏室的门票价格都大于等于0,都有一个快乐值,并且只有进入了一个游戏室,才可以进入它内部的游戏室,小明现在有n元钱,问最大能得到多少的快乐。
11. *基因问题
已知两个基因序列如s:AGTAGT;t:ATTAG。现要你给序列中增加一些空格后,首先使得两个序列的长度相等,其次两个串对应符号匹配得到的值最大。基因只有四种分别用A、G、C、T表示,匹配中不允许两个空格相对应,任意两符号的匹配值由下表给出:
A G C T 〕
A 5 -2 -1 -2 -4
G -2 5 -4 -3 -2
C -1 -4 5 -5 -1
T -2 -3 -5 5 -2
〕 -4 -2 -1 -2
提示:定义问题l[i][j]为取第一个序列的前i项,和第二个序列的前j项,这两个序列加空格匹配的最大值。它的最优值与三个子问题有关,l[i-1][j-1]、l[i][j-1]、l[i-1][j]。
建立递归公式如下:

其中m[0][t[j]表示表中空格和t[j]匹配的对应值。
思考:本问题的初始化。
12. *田忌赛马
田忌与齐王赛马,双方各有n匹马参赛(n<=100),每场比赛赌注为1两黄金,现已知齐王与田忌的每匹马的速度,并且齐王肯定是按马的速度从快到慢出场,现要你写一个程序帮助田忌计算他最好的结果是赢多少两黄金(输用负数表示)。
分析:先排序,齐王的马的速度放在数组a中,田忌的马的速度放在数组b中。本问题应用的算法是动态规划和贪心算法相结合解决的。从两人的最弱的马入手:
若田忌的马快,就让这两匹马比赛;
若田忌的马慢,干脆就让他对付齐王最快的马;
若两匹马的速度相等,这时有两种选择方案,或者它俩比赛,或者对付齐王最快的马。
定义子问题:l(i,j)为齐王的从第i匹马开始的j匹马与田忌的最快的j匹马比赛,田忌所获得的最大收益。
则:
程序具体实现时,为了适合c数据从0开始,稍加变动,定义子问题:l(i,j)为齐王的从第i匹马开始到第i+j匹马共j+1匹马与田忌的最快的j+1匹马比赛,田忌所获得的最大收益。初始化时:l[i][0]表示齐王的第i匹马与田忌最快的马比赛的结果。
程序如下:
#include<stdio.h>
void readdata();
void init();
int n,a[100],b[100],l[100][100];
int main()

int i,j;
readdata();
init();
for(i=n-2;i>=0;i--)
for(j=1;j<n-i;j++)
if(a[i+j]<b[j])
l[i][j]=l[i][j-1]+1;
else if(a[i+j]>b[j])
l[i][j]=l[i+1][j-1]-1;
else if(l[i+1][j-1]-1>l[i][j-1])
l[i][j]=l[i+1][j-1]-1;
else
l[i][j]=l[i][j-1];
printf("%d",l[0][n-1]);

void readdata()

int i;
scanf("%d",&n);
for(i=0;i<n;i++)
scanf("%d",&a[i]);
for(i=0;i<n;i++)
scanf("%d",&b[i]);

void init()

int i;
// qsort(a,n); //略
for(i=0;i<n;i++)

if(a[i]<b[0])
l[i][0]=1;
else if(a[i]==b[0])
l[i][0]=0;
else
l[i][0]=-1;

参考技术A #include <iostream>
#include <string>
using namespace std;
#define N 100 // 宏定义N的初始值为100
char a[N], b[N], str[N]; //a用于保存第一个输入的字符的,b用于保存第二个,str用于判断两个字符是不是都遍历到了'\0'(到了的话说明字符串处理完毕),先初始化为N
int c[N][N]; //int型数组,初始化为N,用于保存两个字符串的内容
//下面你要跟着程序的调用规律走,先看主函数调用的是build_lcs(),然后是lcs_len()
int lcs_len(char* a, char* b,int c[][N]) //用于计算两个字符串的每个元素的内容!

int m=strlen(a), n=strlen(b), i, j; //声明m,n,i,j变量,其中的strlen()函数是用来获取字符串长度的
for (i=0; i<=m; i++) //
c[i][0]=0; //遍历第一个字符串的内容,分别保存到c的一维数组中
for (i=0; i<=n; i++) //
c[0][i]=0; //遍历第二个字符串的内容,分别保存到c的二维数组中
for (i=1; i<=m; i++) //第一层FOR循环
//
for (j=1; j<=n; j++) //第二层FOR循环
//
//
if (a[i-1]==b[j-1]) //判断第一个字符串的第i(-1是为了去掉'\0')个元素的值等于第二个字符串第j个元素
c[i][j]=c[i-1][j-1]+1; //
else if (c[i-1][j]>=c[i][j-1]) //如果不是,则判断第一个字符数组的第i个元素与第二个字符数组所有元素相等(j循环j遍,i才循环1遍)
c[i][j]=c[i-1][j]; //
else //如果不是,则判断第一个字符数组的第j个元素与第二个字符数组所有元素相等(i循环j遍,j才循环1遍)
c[i][j]=c[i][j-1]; //
//
//
return c[m][n]; //得到相同的元素并返回

char* build_lcs(char s[],char* a,char* b)

int i=strlen(a), j=strlen(b);
int k=lcs_len(a,b,c);
s[k]='\0';
while (k>0) //下面都很简单了!

if (c[i][j]==c[i-1][j])
i--;
else if (c[i][j]==c[i][j-1])
j--;
else

s[--k]=a[i-1];
i--; j--;


cout<<s<<endl;
return s;

void main()

cout<<"输入两个长度小于"<<N<<"的字符串"<<endl;
cin>>a;
cin>>b;
cout<<"LCS="<<build_lcs(str,a,b)<<endl;
参考技术B 每行代码都给注释,我只能说你给的分太少。。。
我们也不是苦力

动态规划-最长公共子序列

最长公共子序列(Longest-Common-Subsequences,LCS)是一个在一个序列集合中
(通常为两个序列)用来查找所有序列中最长子序列的问题。
最长公共子串(Longest-Common-Substring,LCS)问题是寻找两个或多个已知字符
串最长的子串。此问题与最长公共子序列问题的区别在于子序列不必是连续的,而子串却
必须是连续的。

思路:假设求最长公共子序列的函数为:MaxLen(i,j),i,j分别是所求2个字符串
S1,S2的长度,利用动态规划的思想,既是把问题规模缩小为一个更小的问题来解决,
从而实现递归的办法。比如考虑MaxLen(i,j)与MaxLen(i-1,j-1)的关系,从而
不断的把问题进而缩小规模。
(1)S1的最后一个元素与S2的最后一个元素相同,这说明该元素一定位于公共子序列中。
因此,现在只需要找:MaxLen(i-1,j-1);假如S1的最后一个元素与S2的最后一个元素相等
MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
(2)假如最后的元素不相等,那说明最后一个元素不可能是最长公共子序列中的元素。
因此会产生两个子问题:MaxLen(i-1,j) 和 MaxLen(i,j-1),最长公共子序列MaxLen(i,j)
的值就从上面2个子问题中取个最大值,
MaxLen(i-1,j)表示:最长公共序列可以在(x1,x2,....x(i-1)) 和 (y1,y2,...yj)中找。
MaxLen(i,j-1)表示:最长公共序列可以在(x1,x2,....xi) 和 (y1,y2,...y(j-1))中找。
求解上面两个子问题,得到的公共子序列谁最长,那谁就是 MaxLen(i,j)。用数学表示就是:
即:MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
(3)边界条件:
MaxLen(n,0) = 0 ( n= 0…len1)
MaxLen(0,n) = 0 ( n=0…len2)
(4)递归中重复计算的问题会影响程序的执行效率,因此通过二维数组来计算,避免重复计算的问题。
就是说原问题 转化 成子问题后,子问题中有相同的问题。
原问题是:LCS(X,Y)。子问题有 ?LCS(Xn-1,Ym-1) ?LCS(Xn-1,Ym) ?LCS(Xn,Ym-1)
初一看,这三个子问题是不重叠的。可本质上它们是重叠的,因为它们只重叠了一大部分。举例:
第二个子问题:LCS(Xn-1,Ym) 就包含了:问题?LCS(Xn-1,Ym-1),为什么?
因为,当Xn-1 和 Ym 的最后一个元素不相同时,我们又需要将LCS(Xn-1,Ym)进行分解:
分解成:LCS(Xn-1,Ym-1) 和 LCS(Xn-2,Ym)也就是说:在子问题的继续分解中,有些问题是重叠的。
由于像LCS这样的问题,它具有重叠子问题的性质,因此:用递归来求解就太不划算了。
因为采用递归,它重复地求解了子问题啊。而且注意哦,所有子问题加起来的个数可是指数级的。
关键是采用动态规划时,并不需要去一一计算那些重叠了的子问题。
或者说:用了动态规划之后,有些子问题 是通过 “查表“ 直接得到的,而不是重新又计算一遍得到的。

例如求fib(5),分解成了两个子问题:fib(4) 和 fib(3),求解fib(4) 和 fib(3)时,又分解了
一系列的小问题....
从中可以看出:根的左右子树:fib(4) 和 fib(3)下,是有很多重叠的!!!比如,对于 fib(2),
它就一共出现了三次。如果用递归来求解,fib(2)就会被计算三次,而用DP(Dynamic Programming)
动态规划,则fib(2)只会计算一次,其他两次则是通过”查表“直接求得。而且,更关键的是:查找
求得该问题的解之后,就不需要再继续去分解该问题了。而对于递归,是不断地将问题分解,直到
分解为基准问题(fib(1) 或者 fib(0))

比如:abcfbc abfcab
0 a b c f b c
0 1 2 3 4 5 6
0 0 0 0 0 0 0 0 0
a 1 0 1 1 1 1 1 1
b 2 0
f 3 0
c 4 0
a 5 0
b 6 0

i=1,j=1时,a=a,那么maxlen(1,1)的值就是maxlen(0,0)+1=1,先完成第1行的二维数组值,
i=1,j=2时,a<>b,取maxLen(0,2),maxLen(1,1)两者的最大值,为1。
的计算,然后再完成第2行,直到计算到最后一行,二维数组中的最大值既是LCS。
(5)当填完表后,通过该二维表从后往前递推,即可求出最长公共子序列。
通过递推公式,可以看出,取maxLen[i][j]取maxLen[i-1][j-1]
或者是maxLen[i-1][j]和maxLen[i][j-1]的较大值(可能相等)。
我们将从最后一个元素maxLen[i][j]倒推出S1和S2的LCS。
maxLen[6][6] = 4,且S1[6] != S2[6],所以倒推回去,maxLen[6][6]的值
来源于maxLen[6][5]或者maxLen[5][6],他们都是4,决定一个方向,后续遇到值相同,都按此方向进行。
maxLen[6][5] = 4, 且S1[5] = S2[6], 所以倒推回去,maxLen[6][5]的值来源于maxLen[5][4]。
以此类推,
如果遇到S1[i] != S2[j] ,且res[i-1][j] = res[i][j-1] 这种存在分支的情况,
这里都选择一个方向(之后遇到这样的情况,也选择相同的方向,要么都往左,要么都往上)。

Python算法实现:
 1 import numpy as np
 2 
 3 def LCS(string1,string2):
 4     len1 = len(string1)
 5     len2 = len(string2)
 6     # i是列-第1个字符串,j是行-第2个字符串
 7     res = [[0 for i in range(len1+1)] for j in range(len2+1)]
 8     for i in range(1,len2+1):
 9         for j in range(1,len1+1):
10             if string2[i-1] == string1[j-1]:
11                 res[i][j] = res[i-1][j-1]+1
12             else:
13                 res[i][j] = max(res[i-1][j],res[i][j-1])
14     # -1,-1是获取数组的最后一个元素的值
15     return res,res[-1][-1]
16 
17 def getLCS(s1,s2,arr,n):
18     # 直接用len求二位数组的维度,返回的是这个数组有多少行nrow,第二个字符串,相当于j
19     j = len(arr)-1
20     # 如果想要求列的话,可以求数组中某一个行向量的列维度ncol,第一个字符串,相当于i
21     i = len(arr[0])-1
22     comlcs = ""
23     while n > 0:
24         if s2[j-1] == s1[i-1]:
25             comlcs += s2[j-1]
26             i -= 1
27             j -= 1
28             n -= 1
29         elif arr[j][i-1] >= arr[j-1][i]:
30             i -= 1
31         elif arr[j-1][i] > arr[j][i-1]:
32             j -= 1
33     return comlcs
34 
35 
36 
37 def main():
38    # 假设这里输入的串都是有公共子序列
39    s1,s2 = input("请分别输入两个字符串,逗号分隔:").split(",")
40    arr,maxLong = LCS(s1,s2)
41    print(np.array(arr))
42    print("最大公共子序列长度:%d" % maxLong)
43    lcs = getLCS(s1,s2,arr,maxLong)
44    # [::-1]倒序输出字符串
45    print("最大公共子序列长度:%s" % lcs[::-1] )
46 
47 
48 
49 if __name__ == "__main__":
50     main()

 

 

以上是关于关于用动态规划法求最大公共子序列的问题的主要内容,如果未能解决你的问题,请参考以下文章

动态规划(最大公共子序列)

算法 LC 动态规划 - 最大递增子序列

动态规划的设计思想与实例(最大子段和最长公共子序列0-1背包编辑距离)

用数学语言说一下动态规划求数列最长递增子序列的解

动态规划求两字符串连续最大公共子串长度

算法复习(四五六)