自我感觉对于动态规划还比较不熟悉,所以专门看了《算法导论》里面第15章,动态规划这一章。对其中的钢条切割、矩阵链乘法、最长公共子序列和最优搜索二叉树这些问题有一定了解。
动态规划是一种解决最优化问题的方法,而不是一种算法。通常需要解决的这些问题有很多解,这些解都有一个值,找出值最大(最小)的解,通常动态规划可以找出最优的值,以及其中一个最优解,无法找出所有的最优解。
动态规划方法求解问题的四个步奏分别是:
1、刻画一个最优解的结构特征
2、递归的定义最优解的值
3、计算最优解的值,(由于代码中的递归操作堆栈开销大所以)一般采用从底层向上的方法,即先求解最小规模问题的最优解,保存这些过程值(避免重复计算已经算过的问题),逐渐求出原问题的最优解的值。
4、构造出最优解(第三步只是先求出最优值,第三步需要记录最优解,提供给第4步求出解)。
下面以一个矩阵链乘法问题为例子
矩阵链是一串可以相乘的矩阵,这个问题是要找出乘法次数最少的相乘组合,即安排矩阵相乘的先后顺序。
假设矩阵链为
假设有个两个相容矩阵行列分别是i*j,j*k,那么乘法次数是i*j*k;
第一步 考虑最优解的结构(要先搞清楚最终解是一个什么样的形式)
可以想到,这个问题是要找出矩阵相乘的组合,比如在这串标号为i到j的矩阵链中,假设中间有个标号k,标号k之前的先计算,k之后的后计算,然后两部分合起来 这么计算乘法次数最少。其实就是加括号的形式,可能括号有很多个。脑子中先有这么一个概念,再进入下一步。
第二步 递归的定义最优解的值。
使用动态规划求解的问题都是可以递归的分割成小问题的,所以要定义最优解的值。如何去定义出这个值呢?这个定义需要包含不止是解的值这么一个信息,而且需要包含是什么问题的解这个信息,所以可以使用一个二维数组来定义最优解的值,m[i][j] = n表示的是矩阵链中标号从i到j需要的乘法次数是n,同时为了第四步能够构造出解,使用另一个二维数组s[i][j] = k表示矩阵链中标号从i到j需要断开(就是加括号)的地方是标号k。当然还要考虑一些边界情况,
比如m[i][j] =
i==j 即只有一个矩阵,那乘法次数为0
i > j 不可能存在这种情况
i < j s[i][j] = k 此时可以递归下去,根据断点k分割成两个子问题,当然最后需要加上k 两边矩阵相乘的乘法次数
所以可以列出一个公式
如此,列出了公式(有的没有公式),而且公式是递归的,算是定义了最优解的值。
第三步计算最优解的值
这一步就涉及到代码如何去实现了,公式是递归的方式定义的,从原始规模开始,不断的变小,这是一种自顶向下的方式,不过考虑到递归次数多的话堆栈开销比较大,所以采取自底向上的方式。
从长度为2(长度为1没有意义)的矩阵链开始考虑,直到整个长度。
需要考虑所有的情况,所以算法中有三层for嵌套。
第一层是矩阵链的长度,
第二层是矩阵链的始末,
第三层是矩阵链的断点,考虑从矩阵链开始,到最后一个的前一个作为断点的情况
使用数组p来表示矩阵链中每个矩阵的行列情况p[i-1]和p[i]分别表示A_i的行和列
矩阵的标号从1开始,矩阵链中矩阵的数量就是数组p元素的个数-1。
下面先写出算法的伪代码
n=p.length-1
let m[1..n,1..n] s[1..n,1..n]be a new tables
for i=1 to n
m[i,i] = 0
for len= 2 to n//长度从2开始
for i = 1 to n-len+1//子序列起点
j = i+len-1;//计算子序列终点
m[i,j] = +max;//初始化为一个极大值
for(k = i to j-1)//考虑每一个都作为断点
t = m[i,k]+m[k+1,j]+p[i-1]*p[k]*p[j]
if(m[i,j] < t)//有更小的就更新掉,就是传说中的 剪切
m[i,j] = t;
s[i,j] = k;
return m[1,n] s
第四步 构造最优解
这一步根据第三步算法中的二维数组s来构造解,这里往往可以采用递归的方式。
找出断点之后可以在断点的两边递归的找下去。输出的结果是矩阵链加括号的形式。
伪代码如下(还是比较绕的)
PRINT(s,i,j)
if i == j//分不下去了,输出这个矩阵
print "A"i
else
print "("//最开始肯定是左括号
k = s[i,j]
PRINT(s,i,k)
PRINT(s,k+1,j)
print ")"//结束肯定是右括号
完整代码如下
#include<iostream>
#include<vector>
using namespace std;
typedef unsigned int uint;
//整形有无符号很重要
const uint max_n = 0xffffffff;
const uint max_row_col = 100;//定义最大的矩阵为100X100
/*
求矩阵链相乘最少的乘法次数
输入:
A 矩阵序列
s 存放最优解的路径
start end 所求矩阵链的 起点到终点 规定矩阵下标从1开始
*/
int matrix_chain_multiply(vector<uint>& A, uint(*s)[max_row_col], uint start, uint end)
{
if (start >= end)
return 0;
uint m[max_row_col][max_row_col];//存放最少的值
uint n = end-start+1;//矩阵链长度
for (uint i = start; i <= end; i++)//,初始化
m[i][i] = 0;
for (uint len = 2; len <= n; len++){//子序列的长度,从2开始,一直到整个序列
for (uint i = start; i <= n - len + start; i++){//子序列的起点,注意终点是相对于start的
uint j = i + len - 1;//子序列的终点(根据子序列的长度确定终点)
m[i][j] = max_n;
for (uint k = i; k < j; k++){//k值在起点和终点之间,不包括终点
uint q = m[i][k] + m[k + 1][j] + (A[i - 1])*(A[k])*(A[j]);
if (m[i][j] > q){
m[i][j] = q;
s[i][j] = k;
}
}
}
}
return m[start][end];
}
void print(uint(*s)[max_row_col],uint i, uint j)
{
if (i == j)
cout << 'A' << i;
else{
uint k = s[i][j];
cout << "(";
print(s, i, k);
print(s, k+1,j);
cout << ")";
}
}
int main()
{
vector<uint> A = { 30, 35, 15, 5, 10, 20, 25 };//矩阵序列,分别表示30X35,35X15, ...20X25的矩阵相乘
uint s[max_row_col][max_row_col];//存放i到j中,左括号为i的左侧,右括号为此值得右侧
int n = A.size() - 1;//矩阵链的总矩阵个数
cout << "1:6 " << matrix_chain_multiply(A, s, 1, n) << endl;
print(s, 1, n);
return 0;
}