实验原理:
1、用动态规划法和备忘录方法实现求两序列的最长公共子序列问题。要求掌握动态规划法思想在实际中的应用,分析最长公共子序列的问题特征,选择算法策略并设计具体算法,编程实现两输入序列的比较,并输出它们的最长公共子序列。
2、用动态规划法和备忘录方法求解矩阵相乘问题,求得最优的计算次序以使得矩阵连乘总的数乘次数最少,并输出加括号的最优乘法算式。
实验内容:
1、最长公共子序列
(1)最长公共子序列(Longest Common Subsequence, LCS)问题是:给定两个字符序列
X={x1,x2,……,xm}和 Y={y1,y2,……,yn},要求找出 A 和 B 的一个最长公共子序列。
例如:X={a,b,c,b,d,a,b},Y={b,d,c,a,b,a}。它们的最长公共子序列 LSC={b,c,b,a}。
通过“穷举法”列出 X 的所有子序列,检查其是否为 Y 的子序列并记录最长公共子序列的
长度这种方法,求解时间为指数级的,因此不可取。
(2)分析 LCS 问题特征可知,如果 Z={z1,z2,……,zk}为它们的最长公共子序列,则它们一定
具有以下性质:
- 若 xm=yn,则 zk=xm=yn,且 Zk-1 是 Xm-1 和 Yn-1 的最长公共子序列;
- 若 xm≠yn且 zk≠xm,则 Z 是 Xm-1 和 Y 的最长公共子序列;
- 若 xm≠yn且 zk≠yn,则 Z 是 X 和 Yn-1 的最长公共子序列。
这样就将求 X 和 Y 的最长公共子序列问题,分解为求解较小规模的子问题:
- 若 xm=yn,则进一步分解为求解两个(前缀)子字符序列 Xm-1 和 Yn-1 的最长公共子列问题;
- 如果 xm≠yn,则原问题转化为求解两个子问题,即找出 Xm-1 和 Y 的最长公共子序列与找出 X 和 Yn-1 的最长公共子序列,取两者中较长者作为 X 和 Y 的最长公共子序列。由此可见,两个序列的最长公共子序列包含了这两个序列的前缀的最长公共子序列,具有最优子结构性质。
(3)令c[i][j]保存字符序列Xi={x1,x2,……,xi}和Yj={y1,y2,……,yj}的最长公共子序列的长度。
由上述分析可得如下递推式:
由此可见,最长公共子序列的求解具有重叠子问题性质,如果采用递归算法实现,会得
到一个指数时间算法。因此需要采用动态规划法自底向上求解,并保存子问题的解,这样可
以避免重复计算子问题,在多项式时间内完成计算。
(4)为了能由最优解值进一步得到最优解(即最长公共子序列),还需要一个二维数组 s[][],
数组中的元素 s[i][j]记录 c[i][j]的值是由三个子问题 c[i-1][j-1]+1,c[i][j-1]和 c[i[1]1][j]中的哪一个计算得到,从而可以得到最优解的当前解分量(即最长公共子序列中的当
前字符),最终构造出最长公共子序列自身。
(5)编程定义 LCS 类,计算最长公共子序列长度,并给出最长公共子序列:
(注意:C 语言中数组下标由 0 开始,而实际数据在一维数组 a、b 和二维数组 c、s 中的存
放却是从下标为 1 处开始。)
类中数据成员主要有二维数组 c 和 s 用于动态规划法求解过程中保存子问题的求解结
果,一维数组 a 和 b 用于存放两个字符序列,m 和 n 为两个字符序列中实际字符的个数。这
些数据成员均应在 LCS 类的构造函数中进行初始化:
代码:
#include <iostream>
#include <string>
using namespace std;
int const MaxLen = 50;
class LCS
{
public:
LCS(int nx, int ny, char* x, char* y)
{
m = nx;
n = ny;
a = new char[m + 2];
b = new char[n + 2];
memset(a, 0, sizeof(a));
memset(b, 0, sizeof(b));
for (int i = 0; i < nx + 2; i++)
a[i + 1] = x[i];
for (int i = 0; i < ny + 2; i++)
b[i + 1] = y[i];
c = new int[MaxLen][MaxLen];
s = new int[MaxLen][MaxLen];
memset(c, 0, sizeof(c));
memset(s, 0, sizeof(s));
}
int LCSLength();
void CLCS()
{
CLCS(m, n);
}
private:
void CLCS(int i, int j);
int(*c)[MaxLen], (*s)[MaxLen];
int m, n;
char* a, * b;
};
int LCS::LCSLength()
{
for (int i = 1; i <= m; i++)
c[i][0] = 0;
for (int j = 1; j <= n; j++)
c[0][j] = 0;
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (a[i] == b[j])
{
c[i][j] = c[i - 1][j - 1] + 1;
s[i][j] = 1;
}
else if (c[i - 1][j] >= c[i][j - 1])
{
c[i][j] = c[i - 1][j];
s[i][j] = 2;
}
else
{
c[i][j] = c[i][j - 1];
s[i][j] = 3;
}
}
}
return c[m][n];
}
void LCS::CLCS(int i, int j)
{
if (i == 0 || j == 0)
return;
if (s[i][j] == 1)
{
CLCS(i - 1, j - 1);
cout << a[i];
}
else if (s[i][j] == 2)
CLCS(i - 1, j);
else
CLCS(i, j - 1);
}
int main() {
int nx, ny;
char *x = new char[MaxLen], *y = new char[MaxLen];
cout << "请输入X (不含空格)" << endl;
scanf("%s", x, sizeof(x));
nx = strlen(x);
cout << "请输入Y (不含空格)" << endl;
scanf("%s", y, sizeof(y));
ny = strlen(y);
LCS lcs(nx, ny, x, y);
cout << "X和Y最长公共子序列的长度为:" << lcs.LCSLength() << endl;
cout << "该序列为" << endl;
lcs.CLCS();
cout << endl;
delete[]x;
delete[]y;
return 0;
}
实验结果:
复杂度分析:
int LCSLength()的平均时间复杂度为O(n);
void CLCS()的平均时间复杂度为O(nlogn)。
2、矩阵连乘
(1)求解目标
若 n 个矩阵{A1,A2,……An}中两个相邻矩阵 Ai和 Ai+1均是可乘的,Ai的维数为 pi×pi+1,
Ai+1 的维数为 pi+1×pi+2。求 n 个矩阵 A1A2……An 连乘时的最优计算次序,以及对应的最少
数乘次数。
两矩阵相乘 AiAi+1 做 pi×pi+1×pi+2 次数乘,可得 pi×pi+2 的结果矩阵。
而矩阵连乘 AiAi+1……Aj(简记为 A[i:j])求得 pi×pj+1 的结果矩阵时,采用不同的计算
次序,对应的总数乘次数也不同。
(2)例如:4 个矩阵连乘 A1A2A3A4,其中 A1 的维数:50×10,A2的维数:10×40,A3 的维数:
40×30,A4的维数:30×5。有 5 种不同的计算次序:
次序 1:(((A1A2)A3)A4) 需要 50×10×40+50×40×30+50×30×5=87500 次
次序 2:((A1A2)(A3A4)) 需要 50×10×40+40×30×5+50×40×5=36000 次
次序 3:((A1(A2A3))A4) 需要 10×40×30+50×10×30+50×30×5=34500 次
次序 4:(A1((A2A3)A4)) 需要 10×40×30+10×30×5+50×10×5=16000 次
次序 5:(A1(A2(A3A4))) 需要 40×30×5+10×40×5+50×10×5=10500 次
(3)将二维数组 m[i][j]定义为:计算 A[i:j]所需的最少数乘次数;
二维数组 s[i][j]定义为:计算 A[i:j]的最优计算次序中的断开位置(例如:若计算 A[i:
j]的最优次序在 Ak 和 Ak+1之间断开,i≤k<j,则 s[i][j]=k)。
(4)当 i=j 时,A[i:j]=Ai是单一矩阵,无须计算,因此 m[i][j]=0;
当 i<j 时,m[i][j]=min{m[i][k]+m[k+1][j]+pipk+1pj+1} (i≤k<j)
(5)算法思路
因为计算 m[i][j]时,只用到已计算出的 m[i][k]和 m[k+1][j]。所以首先计算出 m[i][i]=0,
i=1,2,……n;然后再根据递归式,按矩阵链长递增的方式依次计算 m[i][i+1],i=1,2,…n[1]1(矩阵链长度为 2);m[i][i+2],i=1,2,…n-2(矩阵链长度为 3);……则 m[1][n]就是问题
的最优解值(最少数乘次数)。
要构造问题的最优解,根据 s 数组可推得矩阵乘法的次序。从 s[1][n]可知计算 A[1:n]
的最优加括号方式为(A[1:s[1][n]])(A[s[1][n]+1:n])。其中 A[1:s[1][n]]的最优加括号方式
又为(A[1:s[1][s[1][n]]])(A[s[1][s[1][n]]+1:s[1][n]])。……照此递推下去,最终可以确定
A[1:n]的最优完全加括号方式,构造出问题的一个最优解。
(6)动态规划法实现的算法提示
(请分别对实例 1:A1 维数:50×10,A2 维数:10×40,A3 维数:40×30,A4 维数:
30×5 和实例 2:A1 维数:30×35,A2 维数:35×15,A3 维数:15×5,A4 维数:5×10;
A5 维数:10×20,A6 维数:20×25 分别求解。)
代码:
#include <iostream>
#include <cstring>
#define MAX 1024
using namespace std;
int n;
int p[MAX];
int m[MAX][MAX],s[MAX][MAX];
int MChain() {
for(int r=1; r<n; r++) {
for(int i=0; i<n-r; i++) {
int j = i+r;
m[i][j]=m[i][i]+m[i+1][j]+p[i]*p[i+1]*p[j+1];
s[i][j]=i;
for(int k=i+1; k<j; k++) {
int t = m[i][k]+m[k+1][j]+p[i]*p[k+1]*p[j+1];
if(t<m[i][j]) {
m[i][j]=t;
cout<<"更新m["<<i<<"]["<<j<<"]的值为:"<<t<<endl;
s[i][j]=k;
cout<<"更新s["<<i<<"]["<<j<<"]的值为:"<<k<<endl;
}
}
cout<<"最终求出:m["<<i<<"]["<<j<<"]的值为:"<<m[i][j]<<endl;
}
}
return m[0][n-1];
}
void Traceback(int i,int j) {
if(i==j) {
cout << 'A' << i;
return ;
}
if(i<s[i][j]) cout << '(';
Traceback(i,s[i][j]);
if(i<s[i][j]) cout << ')';
if(s[i][j]+1<j) cout << '(';
Traceback(s[i][j]+1,j);
if(s[i][j]+1<j) cout << ')';
}
int main() {
cin >> n;
for(int i=0; i<=n; i++) {
cin>>p[i];
}
for(int i=0; i<n; i++) {
m[i][i] = 0;
s[i][i]=i;
}
cout << "最少数乘次数:" << MChain() << endl;
cout << "最优计算次序:" << '(';
Traceback(0,n-1);
cout << ')';
return 0;
}
运算结果 :
实验小结:
在本次实验中,我主要学习了最长公共子序列与矩阵连乘的相关知识:
经过代码的学习,我得出最长公共子序列的计算工作模式,如下图所示
关于矩阵连乘我制作了一个矩阵连乘的示意图:
···图损坏了。。。。