趣学算法系列-动态规划-实例分析

来源:互联网 发布:税务网络大学app 编辑:程序博客网 时间:2024/05/16 07:26

趣学算法系列-动态规划-实例分析

声明:本系列为趣学算法一书学习总结内容,在此推荐大家看这本算法书籍作为算法入门,
原作者博客链接,本书暂无免费电子版资源,请大家支持正版,更多实例分析请查看原书内容

第四章 动态规划

  • 实际案例分析-最长公共子序列问题

    • 问题情景

      最长公共子序列问题是指:给定两个序列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 开始{ //如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1c[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;*ons tint 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]){//如果当前字符相同,则公共子序列的长度为该字符前的最长公共序列+1c[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);elseprint(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 << "s1s2 的最长公共子序列长度是: "<<c[len1][len2]<<endl;cout << "s1s2 的最长公共子序列是: ";print(len1,len2); //递归构造最长公共子序列最优解return 0;}


  • 算法时间复杂度分析

(1)时间复杂度:如果两个字符串的长度分别是 m、 n,那么算法时间复杂度为 Ο(m*n)。
(2)空间复杂度:空间复杂度主要为两个二维数组 c[][], b[][],占用的空间为 O(m*n)。

原创粉丝点击