《算法导论》读书笔记

Posted AlanTu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《算法导论》读书笔记相关的知识,希望对你有一定的参考价值。

《算法导论》读书笔记之第15章 动态规划—装配线调度

前言:动态规划的概念

  动态规划(dynamic programming)是通过组合子问题的解而解决整个问题的。分治算法是指将问题划分为一些独立的子问题,递归的求解各个问题,然后合并子问题的解而得到原问题的解。例如归并排序,快速排序都是采用分治算法思想。本书在第二章介绍归并排序时,详细介绍了分治算法的操作步骤,详细的内容请参考:http://www.cnblogs.com/Anker/archive/2013/01/22/2871042.html。而动态规划与此不同,适用于子问题不是独立的情况,也就是说各个子问题包含有公共的子问题。如在这种情况下,用分治算法则会重复做不必要的工作。采用动态规划算法对每个子问题只求解一次,将其结果存放到一张表中,以供后面的子问题参考,从而避免每次遇到各个子问题时重新计算答案。

动态规划与分治法之间的区别:
(1)分治法是指将问题分成一些独立的子问题,递归的求解各子问题
(2)动态规划适用于这些子问题不是独立的情况,也就是各子问题包含公共子问题

  动态规划通常用于最优化问题(此类问题一般有很多可行解,我们希望从这些解中找出一个具有最优(最大或最小)值的解)。动态规划算法的设计分为以下四个步骤:

(1)描述最优解的结构

(2)递归定义最优解的值

(3)按自低向上的方式计算最优解的值

(4)由计算出的结果构造一个最优解

  动态规划最重要的就是要找出最优解的子结构。书中接下来列举4个问题,讲解如何利用动态规划方法来解决。动态规划的内容比较多,我计划每个问题都认真分析,写成日志。今天先来看第一个问题:装配线调度问题

2、问题描述

  一个汽车公司在有2条装配线的工厂内生产汽车,每条装配线有n个装配站,不同装配线上对应的装配站执行的功能相同,但是每个站执行的时间是不同的。在装配汽车时,为了提高速度,可以在这两天装配线上的装配站中做出选择,即可以将部分完成的汽车在任何装配站上从一条装配线移到另一条装配线上。装配过程如下图所示:

  装配过程的时间包括:进入装配线时间e、每装配线上各个装配站执行时间a、从一条装配线移到另外一条装配线的时间t、离开最后一个装配站时间x。举个例子来说明,现在有2条装配线,每条装配线上有6个装配站,各个时间如下图所示:

从图中可以看出按照红色箭头方向进行装配汽车最快,时间为38。分别现在装配线1上的装配站1、3和6,装配线2上装配站2、4和5。

3、动态规划解决步骤

(1)描述通过工厂最快线路的结构

  对于装配线调度问题,一个问题的(找出通过装配站Si,j 最快线路)最优解包含了子问题(找出通过S1,j-1或S2,j-1的最快线路)的一个最优解,这就是最优子结构。观察一条通过装配站S1,j的最快线路,会发现它必定是经过装配线1或2上装配站j-1。因此通过装配站的最快线路只能以下二者之一:

a)通过装配线S1,j-1的最快线路,然后直接通过装配站Si,j

b)通过装配站S2,j-1的最快线路,从装配线2移动到装配线1,然后通过装配线S1,j

为了解决这个问题,即寻找通过一条装配线上的装配站j的最快线路,需要解决其子问题,即寻找通过两条装配线上的装配站j-1的最快线路。

(2)一个递归的解

  最终目标是确定底盘通过工厂的所有路线的最快时间,设为f*,令fi[j]表示一个底盘从起点到装配站Si,j的最快时间,则f* = min(f1[n]+x1,f2[n]+x2)。逐步向下推导,直到j=1。

当j=1时: f1[1] = e1+a1,1,f2[1] = e2+a2,1

当j>1时:f1[j] = min(f1[j-1]+a1,j,f2[j-1]+t2,j-1+a1,j),f2[j] = min(f2[j-1]+a2,j,f1[j-1]+t1,j-1+a2,j)

(3)计算最快时间

  有了递归的解,就可以按照上述的思路编写程序实现,为了避免用递归实现,需要开辟辅助空间来进行,以空间来换取时间,用C语言实现如下所示:

复制代码
 1 void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n)
 2 {
 3     int i,j;
 4     f[0][0] = e[0] + a[0][0];
 5     f[1][0] = e[1] + a[1][0];
 6     l[0][0] = 1;
 7     l[1][0] = 2;
 8     for(j=1;j<n;j++)
 9     {
10         if(f[0][j-1] < f[1][j-1] + t[1][j-1])
11         {
12             f[0][j] = f[0][j-1] + a[0][j];
13             l[0][j] = 1;
14         }
15         else
16         {
17             f[0][j] = f[1][j-1] + t[1][j-1] + a[0][j];
18             l[0][j] = 2;
19         }
20         if(f[1][j-1] < f[0][j-1] + t[0][j-1])
21         {
22             f[1][j] = f[1][j-1] + a[1][j];
23             l[1][j] = 2;
24         }
25         else
26         {
27             f[1][j] = f[0][j-1] + t[0][j-1] + a[1][j];
28             l[1][j] = 1;
29         }
30     }
31     if(f[0][n-1] + x[0] < f[1][n-1] + x[1])
32     {
33         last_f = f[0][n-1] + x[0];
34         last_l = 1;
35     }
36     else
37     {
38         last_f = f[1][n-1] + x[1];
39         last_l = 2;
40     }
41 }
复制代码

(4)构造通过工厂的最快线路

  有第三步骤已经计算出来并记录了每个装配站所在的装配线编号,故可以按照以站号递减顺序直接输出,程序如下所示:

复制代码
 1 void print_station(int l[][N],int last_l,int n)
 2 {
 3     int i = last_l;
 4     int j;
 5     printf("line %d,station %d\\n",i,n);
 6     for(j=n-1;j>0;--j)
 7     {
 8         i = l[i-1][j];
 9         printf("line %d,station %d\\n",i,j);
10     }
11 }
复制代码

  若是按照站号递增顺序输出,则需通过递归进行实现,程序如下所示:

复制代码
 1 void print_station_recursive(int l[][N],int last_l,int n)
 2 {
 3     int i = last_l;
 4     if(n == 1)
 5         printf("line %d,station %d\\n",i,n);
 6     else
 7     {
 8          print_station_recursive(l,l[i-1][n-1],n-1);
 9          printf("line %d,station %d\\n",i,n);
10     }
11 
12 }
复制代码

4、编程实现

根据上面的分析,采用C语言实现如下:

复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 
  4 #define N 6
  5 
  6 void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n);
  7 void print_station(int l[][N],int last_l,int n);
  8 void print_station_recursive();
  9 //全局变量,last_t表示最短时间,last_l表示最后一个装配站所在的装配线编号
 10 int last_f,last_l;
 11 
 12 int main()
 13 {
 14     int a[2][6] = {{7,9,3,4,8,4},{8,5,6,4,5,7}};
 15     int t[2][5] = {{2,3,1,3,4},{2,1,2,2,1}};
 16     int f[2][6] = {0};
 17     int l[2][6] = {0};
 18     int e[2] = {2,4};
 19     int x[2] = {3,2};
 20     int i,j;
 21     fastest_way(a,t,e,x,f,l,6);
 22     //打印输出各个装配线上各个装配站执行的最短时间
 23     for(i=0;i<2;++i)
 24     {
 25         printf("f%d is: ",i+1);
 26         for(j=0;j<6;++j)
 27           printf("%d ",f[i][j]);
 28         printf("\\n");
 29     }
 30     printf("last_f is: %d\\nlast_l is: %d\\n",last_f,last_l);
 31     for(i=0;i<2;++i)
 32     {
 33         printf("l%d is: ",i+1);
 34         for(j=0;j<6;++j)
 35           printf("%d ",l[i][j]);
 36         printf("\\n");
 37     }
 38     print_station(l,last_l,6);
 39     printf("output sequence by recursive.\\n");
 40     print_station_recursive(l,last_l,6);
 41     return 0;
 42 }
 43 
 44 void fastest_way(int a[][N],int t[][N-1],int e[],int x[],int f[][N],int l[][N],int n)
 45 {
 46     int i,j;
 47     f[0][0] = e[0] + a[0][0];
 48     f[1][0] = e[1] + a[1][0];
 49     l[0][0] = 1;
 50     l[1][0] = 2;
 51     for(j=1;j<n;j++)
 52     {
 53         if(f[0][j-1] < f[1][j-1] + t[1][j-1])
 54         {
 55             f[0][j] = f[0][j-1] + a[0][j];
 56             l[0][j] = 1;
 57         }
 58         else
 59         {
 60             f[0][j] = f[1][j-1] + t[1][j-1] + a[0][j];
 61             l[0][j] = 2;
 62         }
 63         if(f[1][j-1] < f[0][j-1] + t[0][j-1])
 64         {
 65             f[1][j] = f[1][j-1] + a[1][j];
 66             l[1][j] = 2;
 67         }
 68         else
 69         {
 70             f[1][j] = f[0][j-1] + t[0][j-1] + a[1][j];
 71             l[1][j] = 1;
 72         }
 73     }
 74     if(f[0][n-1] + x[0] < f[1][n-1] + x[1])
 75     {
 76         last_f = f[0][n-1] + x[0];
 77         last_l = 1;
 78     }
 79     else
 80     {
 81         last_f = f[1][n-1] + x[1];
 82         last_l = 2;
 83     }
 84 }
 85 
 86 void print_station(int l[][N],int last_l,int n)
 87 {
 88     int i = last_l;
 89     int j;
 90     printf("line %d,station %d\\n",i,n);
 91     for(j=n-1;j>0;--j)
 92     {
 93         i = l[i-1][j];
 94         printf("line %d,station %d\\n",i,j);
 95     }
 96 }
 97 void print_station_recursive(int l[][N],int last_l,int n)
 98 {
 99     int i = last_l;
100     if(n == 1)
101         printf("line %d,station %d\\n",i,n);
102     else
103     {
104          print_station_recursive(l,l[i-1][n-1],n-1);
105          printf("line %d,station %d\\n",i,n);
106     }
107 
108 }
复制代码

程序执行结果如下所示:

5、总结

  动态规划是个非常有效的设计方法,要善于用动态规划去分析问题,重点是如何发现子问题的结构。最优子结构在问题域中以两种方式变化(在找出这两个问题的解之后,构造出原问题的最优子结构往往就不是难事了):

a) 有多少个子问题被用在原问题的一个最优解中
b) 在决定一个最优解中使用哪些子问题有多少个选择

 

 

 

《算法导论》读书笔记之第15章 动态规划—矩阵链乘法

前言:今天接着学习动态规划算法,学习如何用动态规划来分析解决矩阵链乘问题。首先回顾一下矩阵乘法运算法,并给出C++语言实现过程。然后采用动态规划算法分析矩阵链乘问题并给出C语言实现过程。

1、矩阵乘法
 
  
 
 
 
  
  从定义可以看出:只有当矩阵A的列数与矩阵B的行数相等时A×B才有意义。一个m×r的矩阵A左乘一个r×n的矩阵B,会得到一个m×n的矩阵C。在计算机中,一个矩阵说穿了就是一个二维数组。一个m行r列的矩阵可以乘以一个r行n列的矩阵,得到的结果是一个m行n列的矩阵,其中的第i行第j列位置上的数等于前一个矩阵第i行上的r个数与后一个矩阵第j列上的r个数对应相乘后所有r个乘积的和。采用C++语言实现完整的两个矩阵乘法,程序如下所示:
复制代码
 1 #include <iostream>
 2 using namespace std;
 3 #define A_ROWS        3
 4 #define A_COLUMNS     2
 5 #define B_ROWS        2
 6 #define B_COLUMNS     3
 7 void matrix_multiply(int A[A_ROWS][A_COLUMNS],int B[B_ROWS][B_COLUMNS],int C[A_ROWS][B_COLUMNS]);
 8 int main()
 9 {
10     int A[A_ROWS][A_COLUMNS] = {1,0,
11                                 1,2,
12                                 1,1};
13     int B[B_ROWS][B_COLUMNS] = {1,1,2,
14                                 2,1,2};
15     int C[A_ROWS][B_COLUMNS] = {0};
16     matrix_multiply(A,B,C);
17     for(int i=0;i<A_ROWS;i++)
18     {
19         for(int j=0;j<B_COLUMNS;j++)
20             cout<<C[i][j]<<" ";
21         cout<<endl;
22     }
23     return 0;
24 }
25 void matrix_multiply(int A[A_ROWS][A_COLUMNS],int B[B_ROWS][B_COLUMNS],int C[A_ROWS][B_COLUMNS])
26 {
27     if(A_COLUMNS != B_ROWS)
28         cout<<"error: incompatible dimensions."<<endl;
29     else
30     {
31         int i,j,k;
32         for(i=0;i<A_ROWS;i++)
33             for(j=0;j<B_COLUMNS;j++)
34             {
35                 C[i][j] = 0;
36                 for(k=0;k<A_COLUMNS;k++)
37                     C[i][j] += A[i][k] * B[k][j]; //将A的每一行的每一列与B的每一列的每一行的乘积求和
38             }
39     }
40 }
复制代码

程序测试结果如下所示:

2、矩阵链乘问题描述

  给定n个矩阵构成的一个链<A1,A2,A3,.......An>,其中i=1,2,...n,矩阵A的维数为pi-1pi,对乘积 A1A2...A以一种最小化标量乘法次数的方式进行加全部括号。

  注意:在矩阵链乘问题中,实际上并没有把矩阵相乘,目的是确定一个具有最小代价的矩阵相乘顺序。找出这样一个结合顺序使得相乘的代价最低。

3、动态规划分析过程

1)最优加全部括号的结构

  动态规划第一步是寻找一个最优的子结构。假设现在要计算AiAi+1....Aj的值,计算Ai...j过程当中肯定会存在某个k值(i<=k<j)将Ai...j分成两部分,使得Ai...j的计算量最小。分成两个子问题Ai...k和Ak+1...j,需要继续递归寻找这两个子问题的最优解。

  有分析可以到最优子结构为:假设AiAi+1....Aj的一个最优加全括号把乘积在Ak和Ak+1之间分开,则Ai..k和Ak+1..j也都是最优加全括号的。

2)一个递归解

  设m[i,j]为计算机矩阵Ai...j所需的标量乘法运算次数的最小值,对此计算A1..n的最小代价就是m[1,n]。现在需要来递归定义m[i,j],分两种情况进行讨论如下:

当i==j时:m[i,j] = 0,(此时只包含一个矩阵)

当i<j 时:从步骤1中需要寻找一个k(i≤k<j)值,使得m[i,j] =min{m[i,k]+m[k+1,j]+pi-1pkpj} (i≤k<j)。

3)计算最优代价

  虽然给出了递归解的过程,但是在实现的时候不采用递归实现,而是借助辅助空间,使用自底向上的表格进行实现。设矩阵Ai的维数为pi-1pi,i=1,2.....n。输入序列为:p=<p0,p1,...pn>,length[p] = n+1。使用m[n][n]保存m[i,j]的代价,s[n][n]保存计算m[i,j]时取得最优代价处k的值,最后可以用s中的记录构造一个最优解。书中给出了计算过程的伪代码,摘录如下:

复制代码
 1 MAXTRIX_CHAIN_ORDER(p)
 2   n = length[p]-1;
 3   for i=1 to n
 4       do m[i][i] = 0;
 5   for t = 2 to n  //t is the chain length
 6        do for i=1 to n-t+1
 7                      j=i+t-1;
 8                      m[i][j] = MAXLIMIT;
 9                      for k=i to j-1
10                             q = m[i][k] + m[k+1][i] + qi-1qkqj;
11                             if q < m[i][j]
12                                then m[i][j] = q;
13                                     s[i][j] = k;
14   return m and s;
复制代码

MATRIX_CHAIN_ORDER具有循环嵌套,深度为3层,运行时间为O(n3)。如果采用递归进行实现,则需要指数级时间Ω(2n),因为中间有些重复计算。递归是完全按照第二步得到的递归公式进行计算,递归实现如下所示:

复制代码
 1 int recursive_matrix_chain(int *p,int i,int j,int m[N+1][N+1],int s[N+1][N+1])
 2 {
 3     if(i==j)
 4        m[i][j] = 0;
 5     else
 6     {
 7         int k;
 8         m[i][j] = MAXVALUE;
 9         for(k=i;k<j;k++)
10         {
11             int temp = recursive_matrix_chain(p,i,k,m,s) +recursive_matrix_chain(p,k+1,j,m,s) + p[i-1]*p[k]*p[j];
12             if(temp < m[i][j])
13             {
14                 m[i][j] = temp;
15                 s[i][j] = k;
16             }
17         }
18     }
19     return m[i][j];
20 }
复制代码

 对递归算计的改进,可以引入备忘录,采用自顶向下的策略,维护一个记录了子问题的表,控制结构像递归算法。完整程序如下所示:

复制代码
 1 int memoized_matrix_chain(int *p,int m[N+1][N+1],int s[N+1][N+1])
 2 {
 3     int i,j;
 4     for(i=1;i<=N;++i)
 5         for(j=1;j<=N;++j)
 6         {
 7            m[i][j] = MAXVALUE;
 8         }
 9     return lookup_chain(p,1,N,m,s);
10 }
11 
12 int lookup_chain(int *p,int i,int j,int m[N+1][N+1],int s[N+1][N+1])
13 {
14     if(m[i][j] < MAXVALUE)
15         return m[i][j]; //直接返回,相当于查表
16     if(i == j)
17         m[i][j] = 0;
18     else
19     {
20         int k;
21         for(k=i;k<j;++k)
22         {
23             int temp = lookup_chain(p,i,k,m,s)+lookup_chain(p,k+1,j,m,s) + p[i-1]*p[k]*p[j];  //通过递归的形式计算,只计算一次,第二次查表得到
24             if(temp < m[i][j])
25             {
26                 m[i][j] = temp;
27                 s[i][j] = k;
28             }
29         }
30     }
31     return m[i][j];
32 }
复制代码

4)构造一个最优解

第三步中已经计算出来最小代价,并保存了相关的记录信息。因此只需对s表格进行递归调用展开既可以得到一个最优解。书中给出了伪代码,摘录如下:

复制代码
1 PRINT_OPTIMAL_PARENS(s,i,j)
2   if i== j 
3      then print "Ai"
4   else
5      print "(";
6      PRINT_OPTIMAL_PARENS(s,i,s[i][j]);
7      PRINT_OPTIMAL_PARENS(s,s[i][j]+1,j);
8      print")";
复制代码

4、编程实现

  采用C++语言实现这个过程,现有矩阵A1(30×35)、A2(35×15)A3(15×5)、A4(5×10)、A5(10×20)、A6(20×25),得到p=<30,35,15,5,10,20,25>。实现过程定义两个二维数组m和s,为了方便计算其第一行和第一列都忽略,行标和列标都是1开始。完整的程序如下所示:

复制代码
 1 #include <iostream>
 2 using namespace std;
 3 
 4 #define N 6
 5 #define MAXVALUE 1000000
 6 
 7 void matrix_chain_order(int *p,int len,int m[N+1][N+1],int s[N+1][N+1]);
 8 void print_optimal_parents(int s[N+1][N+1],int i,int j);
 9 
10 int main()
11 {
12     int p[N+1] = {30,35,15,5,10,20,25};
13     int m[N+1][N+1]={0};
以上是关于《算法导论》读书笔记的主要内容,如果未能解决你的问题,请参考以下文章

《算法导论》读书笔记

《算法导论》读书笔记

《算法导论》读书笔记

《算法导论》读书笔记

算法导论读书笔记-第十三章-红黑树

《算法导论》读书笔记