动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,以至于最后解决原问题需要耗费指数时间。然而,不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,从而得到多项式时间算法。为了达到此目的,可以用一个表来记录所有已解决的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思想。具体的动态规划算法多种多样,但它们具有相同的填表格式。
动态规划算法适用于解最优化问题。通常可按以下4个步骤设计:
(1)找出最优解的性质,并刻画其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
步骤(1)~(3)是动态规划算法的基本步骤。在只需要求出最优值的情形,步骤(4)可以省去。若需要求出问题的最优解,则必须执行步骤(4)。此时,在步骤(3)中计算最优值时,通常需记录更多的信息,以便在步骤(4)中,根据所记录的信息,快速构造出一个最优解。
一.矩阵连乘问题
问题描述
给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2,…,n- 1。计算出这n 个矩阵的连乘积A1A2…An。
分析
矩阵乘法满足结合律,因此计算矩阵的连乘积可以具有不同的计算次序。这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定,也就是说该连乘积已完全加括号,则可依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。
代码
#include <iostream>
using namespace std;
void MatrixChain(int *p,int n,int **m,int **s);
void Traceback(int i, int j, int **s);
int main()
{
const int n = 6;
int p[n + 1] = {30,35,15,5,10,20,25 };
int **m = new int*[n+1];
int **s = new int*[n+1];
for (int i = 0; i < n+1; i++)
{
m[i] = new int[n+1];
s[i] = new int[n+1];
}
MatrixChain(p,n, m, s );
Traceback(1, n, s);
return 0;
}
void MatrixChain(int *p,int n,int **m,int **s)
{ //m是最优值,s是最优值的断开点的索引,n为题目所给的矩阵的个数
for(int i = 1;i<=n;i++) m[i][i] = 0; //对角线为0
for(int r = 2;r<=n;r++){
for(int i = 1;i<=n-r+1;i++){
//从第i个矩阵Ai开始,长度为r,则矩阵段为(Ai~Aj)
int j = i+r-1;//当前矩阵段(Ai~Aj)的起始为Ai,尾为Aj
//求(Ai~Aj)中最小的,其实k应该从i开始,但先记录第一个值,k从i+1开始,这样也可以。
//例如对(A2~A4),则i=2,j=4,下面一行先得出m[2][4]=m[3][4]+p[1]*p[2]*p[4],即A2(A3A4)
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];
s[i][j] = i;//记录断开点的索引
//以下for循环求出(Ai~Aj)中的最小数乘次数
for(int k = i+1 ; k<j;k++){
//例如对(A2~A4),则k=2,此处for循环计算(A2A3)A4 此次for循环不在包含
//A2(A3A4)因为上面已经得出m[2][4]=m[3][4]+p[1]*p[2]*p[4],即A2(A3A4)
//将矩阵段(Ai~Aj)分成左右2部分(左m[i][k],右m[k+1][j]), 不包含A2(A3A4)再加上左右2部分最后相乘的次数(p[i-1] *p[k]*p[j])
int t = m[i][k] + m[k+1][j] + p[i-1] *p[k]*p[j];
if(t<m[i][j]) { m[i][j] = t; s[i][j] = k; //保存最小的断开位置,即最优的结果
}//if
}//k
}//i
}//r
}
void Traceback(int i, int j, int **s)
{
if (i == j)
{
cout << "A" << i;
return;
}
cout << "(";
Traceback(i, s[i][j], s);
Traceback(s[i][j] + 1, j, s);
cout << ")";
}
运行结果
二. 最长公共子序列问题
问题描述
给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
例如,若X= {A,B,C,B,D,A,B},Y = {B,D,C,A,B,A}则序列{B,C,A}是X和Y的-一个公共子序列,但它不是X和Y的一个最长公共子序列。序列{B,C,B,A}也是X和Y的一个公共子序列,它的长度为4,而且它是X和Y的最长公共子序列,因为X和Y没有长度大于4的公共子序列。
代码
#include <iostream>
using namespace std;
#include <cstring>
const int MAX = 100;
void LCSLength(char *x, char *y, int m, int n, int c[][MAX], int b[][MAX])
{
int i, j;
for (i = 0; i <= m; i++) //从0开始遍历
c[i][0] = 0; //当i或者j为0时,最长公共子序列为空
for (j = 1; j <= n; j++)
c[0][j] = 0; //当i或者j为0时,最长公共子序列为空
for (i = 1; i <= m; i++)
{
for (j = 1; j <= n; j++)
{
if (x[i - 1] == y[j - 1]) //遍历到两个值相等
{
c[i][j] = c[i - 1][j - 1] + 1; //向左上方遍历
b[i][j] = 0; //标记
}
else if (c[i - 1][j] >= c[i][j - 1]) //取大的
{
c[i][j] = c[i - 1][j]; //向上
b[i][j] = 1;
}
else
{
c[i][j] = c[i][j - 1]; //向左
b[i][j] = -1;
}
}
}
}
void PrintLCS(int b[][MAX], char *x, int i, int j)
{
if (i == 0 || j == 0)
return;
if (b[i][j] == 0)
{
PrintLCS(b, x, i - 1, j - 1);
cout << x[i - 1] << " ";
}
else if (b[i][j] == 1)
PrintLCS(b, x, i - 1, j);
else
PrintLCS(b, x, i, j - 1);
}
int main()
{
char x[MAX] = { "abcdefggigk" };
char y[MAX] = { "acdefhglkm" };
int b[MAX][MAX];
int c[MAX][MAX];
int m, n;
m = strlen(x);
n = strlen(y);
//保存两个序列的长度
LCSLength(x, y, m, n, c, b);
PrintLCS(b, x, m, n);
cout << endl;
return 0;
}
运行结果
三. 流水作业调度问题
问题描述
n个作业{1,2,…,n}要在由2台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工,然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi,1≤i≤n。流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。
分析
一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少。在一般情况下,机器M2上会有机器空闲和作业积压两种情况。
设全部作业的集合为N = {1,2,…,n}.S⊆N是N的作业子集。在一般情况下,机器M1开始加工S中作业时,机器M2还在加工其他作业,要等时间t后才可利用。这种情况下完成S中作业所需的最短时间记为T(S,t)。流水作业调度问题的最优值为T(N,0)。
代码
//#include "stdafx.h"
#include <iostream>
using namespace std;
const int N = 5;
class Jobtype
{
public:
int operator <=(Jobtype a) const
{
return(key<=a.key);
}
int key,index;
bool job;
};
int FlowShop(int n,int a[],int b[],int c[]);
void BubbleSort(Jobtype *d,int n);//本例采用冒泡排序
int main()
{
int a[] = {2,4,3,6,1};
int b[] = {5,2,3,1,7}; //分别表示在两个机器上的使劲
int c[N]; //存放调度的顺序
int minTime = FlowShop(N,a,b,c);
cout<<"完成作业的最短时间为:"<<minTime<<endl;
cout<<"编号从0开始,作业调度的顺序为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<c[i]<<" ";
}
cout<<endl;
return 0;
}
int FlowShop(int n,int a[],int b[],int c[])
{
Jobtype *d = new Jobtype[n];
for(int i=0; i<n; i++)
{
d[i].key = a[i]>b[i]?b[i]:a[i];
//按Johnson法则分别取对应的b[i]或a[i]值作为关键字
d[i].job = a[i]<=b[i];
//给符合条件a[i]<b[i]的放入到N1子集标记为true
d[i].index = i;
}
BubbleSort(d,n);//对数组d按关键字升序进行排序
int j = 0,k = n-1;
for(int i=0; i<n; i++)
{
if(d[i].job)
{
c[j++] = d[i].index;
//将排过序的数组d,取其中作业序号属于N1的从前面进入
}
else
{
c[k--] = d[i].index;
//属于N2的从后面进入,从而实现N1的非减序排序,N2的非增序排序
}
}
j = a[c[0]];
k = j+b[c[0]];
for(int i=1; i<n; i++)
{
j += a[c[i]];//M1在执行c[i]作业的同时,M2在执行c[i-1]号作业,最短执行时间取决于M1与M2谁后执行完
k = j<k?k+b[c[i]]:j+b[c[i]];//计算最优加工时间
}
delete d;
return k;
}
//冒泡排序
void BubbleSort(Jobtype *d,int n)
{
int i,j,flag;
Jobtype temp;
for(i=0;i<n;i++){
flag = 0;
for(j=n-1;j>i;j--){
//如果前一个数大于后一个数,则交换
if(d[j]<=d[j-1]){
temp = d[j];
d[j] = d[j-1];
d[j-1] = temp;
flag = 1;
}
}
//如果本次排序没有进行一次交换,则break,减少了执行之间。
if(flag == 0){
break;
}
}
}
运行结果
四. 大币找零钱问题
问题描述
有多种不同方式的硬币,需要对用户进行找零服务,例如,用户需要找零18元,指定三种买呢的硬币,分别是5元,2元,1元,在此情况下,用户得到的最优找零数为5张(三枚五元,一枚两元,一枚一元)
代码
#include<iostream>
using namespace std;
int main() {
//硬币面值的数组
int values[100];
//要找零的数
int money,size;
//保存每个面值对应的最小值 因为0号位置要舍弃 因此要加1
//输入总共有几种面值 和从小到大输入每种面值的大小
cout<<"请输入要找零的金额";
cin>>money;
cout<<"请输入要找回面值的分类数";
cin>>size;
cout<<"请输入要找回面值的种类";
for(int i = 0; i < size; i++)
cin >> values[i];
int *coinsUsed = new int[money + 1];
coinsUsed[0] = 0;
//总共的钱数的遍历
int mincoin = 0;
for(int i = 1; i <= money; i++){
mincoin = i;
//总共有几种面值的遍历
for (int j = 0; j < size; j++) {
//如果当前第j中面值小于总共的钱数
if(values[j] <= i){
//用了这一面值 前提是用这张纸币的数量比不用这张纸币的数量小采用
mincoin = min(mincoin,coinsUsed[i - values[j]] + 1);
}
}
//当前金额所需的最小纸张数
coinsUsed[i] = mincoin;
}
cout<<"总共需要面币张数为"<<coinsUsed[money]<<endl;
return 0;
}
运行结果
参考文献 《计算机算法设计与分析(第四版)》 王晓东 编著