趣学算法系列-动态规划-实例分析
声明:本系列为趣学算法一书学习总结内容,在此推荐大家看这本算法书籍作为算法入门,
原作者博客链接,本书暂无免费电子版资源,请大家支持正版,更多实例分析请查看原书内容
第四章 动态规划
-
实际案例分析-最长公共子序列问题
- 问题情景
最长公共子序列问题是指:给定两个序列X={x1, x2, x3, …, xm}和
Y={y1, y2, y3, …,yn},找出 X 和 Y 的一个最长的公共子序列- 问题分析
例如: X=( A, B, C, B, A, D, B), Y=( B, C, B, A, A, C),那么最长公共子序列是 B, C, B, A。
如何找到最长公共子序列呢?
直接想法 暴力求解(一般问题的正确思路都是从最直接的想法慢慢优化的过程)
穷举 X 的所有子序列,检查每个子序列是否也是 Y 的子序列,记录找到的最长公共子序列。
X序列的子序列共有2的m次方个(不知如何计算的请自行百度),此方法的时间复杂度为指数阶动态规划的思路
(1)分析最优解的结构特征
假设已经知道 Zk={z1, z2, z3,…, zk}是 Xm={x1, x2, x3,…, xm}和 Yn={y1, y2, y3,…,
yn}的最长公共子序列。这个假设很重要,我们都是这样假设已经知道了最优解
那么可以分 3 种情况讨论
1: xm= yn= zk:那么 Zk−1={z1, z2, z3,…, zk−1}是 Xm−1 和 Yn−1 的最长公共子序列
2: xm≠yn, xm≠ zk:我们可以把 xm 去掉,那么 Zk 是 Xm−1 和 Yn 的最长公共子序列
3: xm≠yn, yn≠ zk:我们可以把 yn 去掉,那么 Zk 是 Xm 和 Yn−1的最长公共子序列
(2)建立最优值的递归式
设 c[i][j]表示 Xi 和 Yj 的最长公共子序列长度。
1: xm= yn= zk:那么 c[i][j]= c[i−1][j−1]+1;
2: xm≠yn:那么我们只需要求解 Xi 和 Yj−1 的最长公共子序列和 Xi−1 和 Yj 的最长公共子
序列,即 c[i][j]= max{c[i][j−1],c[i−1][j]}。
-最长公共子序列长度递归式
(3) 自底向上计算最优值,并记录最优值和最优策略
(4)构造最优解
求解过程只是得到了最长公共子序列长度,并不知道最长公共子序列是什么
例如,现在已经求出 c[m][n]=5,表示 Xm 和 Yn 的最长公共子序列长度是 5,那么这个 5
是怎么得到的呢?我们可以反向追踪 5 是从哪里来的。
xi= yj 时: c[i][j]= c[i−1][j−1]+1;
xi≠yj 时: c[i][j]= max{c[i][j−1], c[i−1][j]};
那么 c[i][j]的来源一共有 3 个: c[i][j]= c[i−1][j−1]+1, c[i][j]= c[i][j−1], c[i][j]= c[i−1][j]。
在第 3 步自底向上计算最优值时,用一个辅助数组 b [i][j]记录这 3 个来源:
c[i][j]= c[i−1][j−1]+1, b[i][j]=1;
c[i][j]= c[i][j−1], b[i][j]=2;
c[i][j]= c[i−1][j], b[i][j]=3
这样就可以根据 b[i][j]反向追踪最长公共子序列,当 b[i][j]=1 时,输出 xi;当 b [i][j]=2
时,追踪 c[i][j−1];当 b[i][j]=3 时,追踪 c[i−1][j],直到 i=0 或 j=0 停止-
算法设计
(1)确定合适的数据结构
采用二维数组 c[][]来记录最长公共子序列的长度, 二维数组 b[][]来记录最长公共子序列
的长度的来源
(2)初始化
输入两个字符串 s1、 s2,初始化 c[][]第一行第一列元素为 0(第一行第一列分别表示两个字符串为0个字符)
(3)循环阶段
• i = 1: s1[0]与 s2[j−1]比较, j=1, 2, 3,…, len2。
如果 s1[0]=s2[j−1], c[i][j] = c[i−1][j−1]+1;并记录最优策略来源 b[i][j]=1;
如果 s1[0] ≠s2[j−1],则公共子序列的长度为 c[i][j−1]和 c[i−1][j]中的最大值,如果 c[i][j−1]≥
c[i−1][j],则 c[i][j]=c[i][j−1],最优策略来源 b[i][j]=2;否则 c[i][j]= c[i−1][j],最优策略来源
b[i][j]=3。
• i = 2: s1[1]与 s2[j−1]比较, j=1, 2, 3,…, len2。
• 以此类推,直到 i > len1 时,算法结束,这时 c[len1][len2]就是最长公共序列的
长度。
(4)构造最优解
根据最优决策信息数组 b[][]递归构造最优解,即输出最长公共子序列。因为我们在求最
长公共子序列长度 c[i][j]的过程中,用 b[i][j]记录了 c[i][j]的来源,那么就可以根据 b[i][j]数
组倒推最优解。
(可以求出最优解的开头和当前位置的长度信息得出最优解具体信息,也可以直接倒序输出最后逆置)- 完美图解
以字符串 s1=“ ABCADAB”, s2=“ BACDBA”为例。
(1)初始化
len1=7, len2=6, 初始化 c[][]第一行、 第一列元素为 0,
( 2) i=1: s1[0]与 s2[j−1]比较, j=1, 2, 3,…, len2。
即“ A”与“ BACDBA”分别比较一次
如果字符相等, c[i][j]取左上角数值加 1,记录最优值
来源 b[i][j]=1。
如果字符不等,取左侧和上面数值中的最大值。如果左侧和上面数值相等,默认取左侧
数值。如果 c[i][j]的值来源于左侧 b[i][j]=2,来源于上面 b[i][j]=3。
• j=1: A≠B,左侧=上面,取左侧数值, c[1][1]= 0,最优策略来源 b[1][1]=2,如图 4-8
所示。
• j=2: A=A,则取左上角数值加 1, c[1][2]= c[0][1]+1=2,最优策略来源 b[1][2] =1,
如图 4-9 所示。后续略
(3) i=2: s1[1]与 s2[j−1]比较, j=1, 2, 3,…, len2。即“ B”与“ BACDBA”分别比较
一次。
如果字符相等, c[i][j]取左上角数值加 1,记录最优值来源 b[i][j]=1。
如果字符不等,取左侧和上面数值中的最大值。如果左侧和上面数值相等,默认取左侧
数值。如果 c[i][j]的值来源于左侧 b[i][j]=2,来源于上面 b[i][j]=3,如图 4-14 所示。
(5)构造最优解
- 完美图解
-
伪代码详解
(1)最长公共子序列求解函数Void LCSL() { int I,j; for(I = 1;I <= len1;i++) //控制 s1 序列 for(j = 1;j <= len2;j++) //控制 s2 序列 { if(s1[i-1]==s2[j-1]) //字符下标从 0 开始 { //如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1 c[i][j] = c[i-1][j-1]+1; b[i][j] = 1; } else { if(c[i][j-1]>=c[i-1][j]) //两者找最大值,并记录最优策略来源 { c[i][j] = c[i][j-1]; b[i][j] = 2; } else { c[i][j] = c[i-1][j]; b[i][j] = 3; } } } }
(2)最优解输出函数
Void print(int I, int j)//根据记录下来的信息构造最长公共子序列(从 b[i][j]开始递推) { if(i==0 || j==0) return; if(b[i][j]==1) { print(i-1,j-1); cout<<s1[i-1]; } else if(b[i][j]==2) print(I,j-1); else print(i-1,j); }
-
实战演练
//program 4-1
#include <iostream>
#include<cstring>
using namespace std;
const int N=1002;
int c[N][N],b[N][N];
char s1[N],s2[N];
int len1,len2;
void LCSL()
{
int I,j;
for(I = 1;I <= len1;i++)//控制 s1 序列
for(j = 1;j <= len2;j++)//控制 s2 序列
{
if(s1[i-1]==s2[j-1])
{//如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1
c[i][j] = c[i-1][j-1]+1;
b[i][j] = 1;
}
else
{
if(c[i][j-1]>=c[i-1][j])
{
c[i][j] = c[i][j-1];
b[i][j] = 2;
}
else
{
c[i][j] = c[i-1][j];
b[i][j] = 3;
}
}
}
}
void print(int I, int j)//根据记录下来的信息构造最长公共子序列(从 b[i][j]开始递推)
{
if(i==0 || j==0) return;
if(b[i][j]==1)
{
print(i-1,j-1);
cout<<s1[i-1];
}
else if(b[i][j]==2)
print(I,j-1);
else
print(i-1,j);
}
int main()
{
int I,j;
cout << "输入字符串 s1: "<<endl;
cin >> s1;
cout << "输入字符串 s2: "<<endl;
cin >> s2;
len1 = strlen(s1);//计算两个字符串的长度
len2 = strlen(s2);
for(I = 0;I <= len1;i++)
{
c[i][0]=0;//初始化第一列为 0
}
for(j = 0;j<= len2;j++)
{
c[0][j]=0;//初始化第一行为 0
}
LCSL(); //求解最长公共子序列
cout << "s1 和 s2 的最长公共子序列长度是: "<<c[len1][len2]<<endl;
cout << "s1 和 s2 的最长公共子序列是: ";
print(len1,len2); //递归构造最长公共子序列最优解
return 0;
}
- 算法时间复杂度分析
>(1)时间复杂度:如果两个字符串的长度分别是 m、 n,那么算法时间复杂度为 Ο(m*n)。(2)空间复杂度:空间复杂度主要为两个二维数组 c[][], b[][],占用的空间为 O(m*n)。