动态规划
算法总体思想
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题。
但是经分解得到的子问题往往不是互相独立的。不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。
如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
动态规划基本步骤:
(1)找出最优解的性质,并刻划其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
实例一、完全加括号的矩阵连乘积
问题可递归定义:
(1)单个矩阵是完全加括号的;
(2)矩阵连乘积A是完全加括号的 ,则A可表示为2个完全加括号的矩阵连乘积B和C的乘积并加括号,即 A = (BC)。
设有四个矩阵A,B,C,D它们的维数分别是: A = 50*10 , B = 10*40 , C = 40*30 , D = 30*5
总共有五中完全加括号的方式:
例如:((A(BC))D): 10 * 40 * 30 + 10 * 30 * 50 + 50 * 30 * 5 = 34500
给定矩阵{A1, A2, A3,..., An},其中Ai与A(i+1)是可乘的。i = 1,2,3, ..., n - 1。考察这n个矩阵的连乘积A1*A2*A3...An.
由于矩阵乘法满足结合律,所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。
若一个矩阵连乘积的计算次序完全确定,也就是说该连乘积已完全加括号,则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。
矩阵连乘问题
给定矩阵{A1, A2, A3,..., An},其中Ai与A(i+1)是可乘的。i = 1,2,3, ..., n - 1。考察这n个矩阵的连乘积A1*A2*A3...An. 如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少.
穷举法:列举出所有可能的计算次序,并计算出每一种计算次序相应需要的数乘次数,从中找出一种数乘次数最少的计算次序。
算法复杂度分析:
对于n个矩阵的连乘积,设其不同的计算次序为P(n)
由于每种加括号方式都可以分解为两个子矩阵的加括号问题
(A1...Ak)(A(k+1)…An)可以得到关于P(n)的递推式如下:
动态规划:将矩阵连乘积A(i)A(i+1)…A(j)简记为A[i:j],这里 i <= j。
考察计算A[i:j]的最优计算次序。设这个计算次序在矩阵A(k)和A(k+1)之间将矩阵链断开,i <= k < j, 则其相应完全加括号方式为(A(i)A(i+1)...A(k)) * (A(k+1)A(k+2)...A(j))。
计算量:A[i:k]的计算量加上A[k+1,j],再加上A[i:k] * A[k+1][j]的计算量。
分析最优解的结构
特征:计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。
建立递归关系
设计算A[i:j],1 <= i <= j <= n,所需要的最少数乘次数m[i,j],则原问题的最优值为m[1,n]
当i = j时,A[i:j]=Ai,因此,m[i,i] = 0,i = 1,2,…,n
当i < j时,m[i,j] = m[i,k] + m[k+1,j] + p(i-1)p(k)p(j)
这里A(i)的维数为p(i-1)*(i)(注:p(i-1)为矩阵A(i)的行数,p(i)为矩阵A[i]的列数)
可以递归地定义m[i,j]为:
k的位置只有j - i种。
计算最优值
对于1 <= i <= j <= n不同的有序对(i,j)对应于不同的子问题。因此,不同子问题的个数最多只有:
(大括号表示C(n,2),组合的意思。后面的符号表示 “紧渐近界记号”)
但是,在递归计算时,许多子问题被重复计算多次。这也是该问题可用动态规划算法求解的又一显著特征。
用动态规划算法解此问题,可依据其递归式以自底向上的方式进行计算。在计算过程中,保存已解决的子问题答案。每个子问题只计算一次,而在后面需要时只要简单查一下,从而避免大量的重复计算,最终得到多项式时间的算法。
用动态规划法求最优解
连乘矩阵假如为:
计算过程为:
从m可知最小连乘次数为m[1][6] = 15125
从s可知计算顺序为((A1(A2A3))((A4A5))A6))
实现:
// DynamicGui.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
class matrix_chain{
public:
matrix_chain(vector<int> &c){
row = c;
count = c.size();//?为什么不减1
m.resize(count);
s.resize(count);
for(int i = 0;i < count;i++){
m[i].resize(count);
s[i].resize(count);
}
for(int i = 0;i < count;i++){
for(int j = 0;j < count;j++){
m[i][j] = 0;
s[i][j] = 0;
}
}
}
//普通方法:
void caculate(){
int n = count -1;
int r,i,j,k;
for(r = 2;r<= n;r++)
{
for(i = 1;i <= n-r+1;i++){
j = i+r-1;
//
m[i][j] = m[i+1][j] + row[i-1]*row[i]*row[j];
s[i][j] = i;
for(k = i+1;k <j;k++){
int temp = m[i][k] + m[k+1][j] +row[i-1]*row[k]*row[j];
if(temp <m[i][j]){
m[i][j] = temp;
s[i][j] = k;
}
}
}
}
min_count = m[1][n];
cout << "min_multi_count"<<min_count<<endl;
_trackback(1,n);
}
void lookup_chain(){
_lookup_chain(1,count-1);
min_count = m[1][count-1];
cout << "min_multi_count ="<<min_count<<endl;
//
int n = count-1;
_trackback(1,count-1);
for(int i =1;i <=n;i++){
for(int j= 1;j <=n;j++){
cout << setw(6)<<m[i][j];
}
cout <<endl;
}
}
private:
int _lookup_chain(int i,int j){
if(m[i][j] > 0)
return m[i][j];
if(i == j)
return 0;
int u = _lookup_chain(i,i)+_lookup_chain(i+1,j)+row[i-1]*row[i]*row[j];
s[i][j] = i;
for(int k = i+1;k <j;k++){
int temp = _lookup_chain(i,k)+_lookup_chain(k+1,j)+row[i-1]*row[k]*row[j];
if(temp < u)
{
u = temp;
s[i][j] = k;
}
}
m[i][j] = u;
return u;
}
void _trackback(int i,int j){
if(i == j)
return;
_trackback(i,s[i][j]);
_trackback(s[i][j]+1,j);
cout <<i << "," << s[i][j] << " " << s[i][j] + 1 << "," << j << endl;
}
private:
vector<vector<int>> m;
vector<vector<int>> s;
vector<int> row;
int count;
int min_count;
};
int main()
{
const int MAX_COUNT = 6;
vector<int> c(MAX_COUNT+1);
c[0] = 30;
c[1] = 35;
c[2] = 15;
c[3] = 5;
c[4] = 10;
c[5] = 20;
c[6] = 25;
matrix_chain mc(c);
//mc.caculate();
mc.lookup_chain();
system("pause");
return 0 ;
}
算法复杂度分析:
算法matrixChain的主要计算量取决于算法中对r,i和k的3重循环。循环体内的计算量为O(1),而3重循环的总次数为O(n^3)。因此算法的计算时间上界为O(n^3)。算法所占用的空间显然为O(n^2)。
动态规划算法的基本要素
一、最优子结构
矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。
在分析问题的最优子结构性质时,所用的方法具有普遍性:首先假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾。
利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。最优子结构是问题能用动态规划算法求解的前提。
同一个问题可以有多种方式刻划它的最优子结构,有些表示方法的求解速度更快(空间占用小,问题的维度低)
二、重叠子问题
递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。这种性质称为子问题的重叠性质。
动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。
通常不同的子问题个数随问题的大小呈多项式增长。因此用动态规划算法只需要多项式时间,从而获得较高的解题效率。
三、备忘录方法
备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
实现(见矩阵连乘源码)
实例二、最长公共子序列
若给定的序列X = {x1,x2,…,xm},则另一序列Z = {z1,z2,…,zk},是X的子序列是指存在一个严格下表序列{i1,i2,…,ik}使得对于所有的j = 1,2,…k有zj = xij。例如,序列Z = {B,C,D,B}是序列X = {A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}。
给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
问题表述:给定2个序列X={x1,x2,…,xm}和Y = {y1,y2,…,yn},找出X和Y的最长公共子序列。
最长公共子序列的结构
设序列X = {x1,x2,…,xm}和Y = {y1,y2,…,yn}的最长公共子序列为Z = {z1,z2,…,zk} ,则
(1)若xm = yn,则zk = xm = yn,且z(k-1)是x(m-1)和y(n-1)的最长公共子序列。
(2)若xm != yn且zk != xm,则Z是x(m-1)和Y的最长公共子序列。
(3)若xm != yn且zk != yn,则Z是X和y(n-1)的最长公共子序列。
由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。
子问题的递归结构
由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用c[i][j]记录序列Xi和Yi的最长公共子序列的长度。其中, Xi={x1,x2,…,xi};Yj={y1,y2,…,yj}。当i = 0或j = 0时,空序列是Xi和Yj的最长公共子序列。故此时C[i][j] = 0。其它情况下,由最优子结构性质可建立递归关系如下:
由于在所考虑的子问题空间中,总共有θ(mn)个不同的子问题,因此,用动态规划算法自底向上地计算最优值能提高算法的效率
计算最优值和构造最长公共子序列(见源码)
实现:
// DynamicGui2.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
class matrix_chain{
public:
matrix_chain(const vector<char> &c1,const vector<char> &c2)
{
A = c1;
B = c2;
lenA = A.size();
lenB = B.size();
c.resize(lenA +1);
for(int i= 0;i<= lenA;i++ )
c[i].resize(lenB+1);
for(int i = 0;i <=lenA;i++)
for(int j = 0;j <= lenB;j++)
c[i][j] = 0;
}
//备忘录这里有个问题,并不是C中所有的元素都被赋值了,如何解决呢?
int cac(int i,int j)
{
int len;
if(i==0||j==0)
return 0;
if(c[i][j]> 0)
return c[i][j];
if(A[i-1] == B[j-1])
{
len = cac(i-1,j-1)+1;
}
else
{
if(cac(i-1,j) > cac(i,j-1))
len = cac(i-1,j);
else
len = cac(i,j-1);
}
c[i][j] = len;
return len;
}
void show()
{
for(int i= 0;i <=lenA;i++)
{
for(int j= 0;j <= lenB;j++)
{
cout << setw(3)<<c[i][j];
}
cout << endl;
}
for(int i =1;i<=lenB;i++)
{
if(c[6][i] > c[6][i-1])
cout << setw(3)<< B[i-1];
}
}
// 普通计算方法;
void caculate(){
for(int i=1;i <= lenB;i++)
{
for(int j = 1;j<= lenA;j++)
{
if(A[j-1] == B[i-1])
c[j][i] = c[j-1][i-1]+1;
else
{
if(c[j-1][i]>c[j][i-1])
c[j][i] = c[j-1][i];
else
c[j][i] = c[j][i-1];
}
}
}
}
private:
vector<vector<int>> c;
int lenA,lenB;
vector<char> A;
vector<char> B;
};
int _tmain(int argc, _TCHAR* argv[])
{
vector<char> c1;
vector<char> c2;
c1.push_back('A');
c1.push_back('B');
c1.push_back('C');
c1.push_back('D');
c1.push_back('E');
c1.push_back('F');
c2.push_back('B');
c2.push_back('D');
c2.push_back('F');
c2.push_back('G');
c2.push_back('H');
matrix_chain mc(c1,c2);
//mc.caculate();
mc.cac(c1.size(),c2.size());
mc.show();
system("pause");
return 0;
}