动态规划问题总结
一.动态规划问题特征:
---最优子结构性质:最优解包含子问题最优解
---重叠子问题性质:求解过程会重复求解子问题
二.矩阵连乘问题
1.问题描述:
![]()
![]()
2.特征:最优子结构:假设连乘最优解在k处断开,则最优解的连乘方式也包括A[i,k-1]和A[k,j]的最优解
重叠子问题:在计算不同的断开点所需的计算量时,会出现多次重复的计算
3.解决方案:
①递归+备忘录算法:
a.建立递归方程:


即计算最优解断开处k的连乘次数,需要递归运算i~k,k+1~j最优解断开处的连乘次数。
b.备忘录:
内容:每次计算连乘次数后保留连乘的矩阵下标和连乘次数
作用:降低时间复杂度(降低前递归算法时间复杂度:O(2^n))
---算法分析:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
---------------------------------------------------------具体代码+注释----------------------------------------------------------------
#include<bits/stdc++.h>
using namespace std;
int m[2000][2000]; //设置m数组,m[i][j]表示矩阵i~j的最小连乘次数
int s[2000][2000]; //记录最优断点,s[i][j]表示矩阵i~j的最优解断点位置
int p[2000]; //p[i-1]为矩阵i的行数;p[i]为Ai的列数
int n; //矩阵个数
int RecurMatrixChain(int i, int j)
void print(int i, int j) ;
int main(){
cin>>n;
for(int i=1;i<=n;i++) //设置下标1~n方便后续计算
for(int j=1;j<=n;j++) m[i][j]=0; //m数组初始化
cout<<RecurMatrixChain(1,n)<<endl;
print(1,n)
return 0;
}
int RecurMatrixChain(int i,int j){
if(i==j) return 0; //i,j相等无意义
if(m[i][j]>0) return m[i][j]; //m初始值为0,若m[i][j]>0,则为已经计算好的i~j最优解(备忘录)
m[i][j]=RecurMatrixChain(i+1,j)+p[i-1]*p[i]*p[j]; //m[i][j]的初值为断点为i时的最少计算量
s[i][j]=i; //记录初始断点
for(int k=i+1;k<j;k++){ //尝试后续断点,寻找最优解
int t=RecurMatrixChain(i,k)+RecurMatrixChain(k+1,j)+p[i-1]*p[k]*p[j];
if(t<m[i][j]) {m[i][j]=t;s[i][j]=k;} //如果计算量更小则刷新m和s数组
}
return m[i][j];
}
void print(int i,int j){ //打印最优解方案
if(i==j) cout<<"A"<<i;
else{
int k=s[i][j];
cout<<"(";
print(i,k); //递归打印子问题中的矩阵和括号
print(k+1,j);
cout<<")";
}
}
②填表法

a.填表分析:
表的维度:n*n------------------存放m,s数组
表的范围:上三角-------------i<j
填表顺序:对角线开始,自下而上,从左到右(如图)
最优解位置:右上角元素

---算法分析:
时间复杂度:O(n^3)
空间复杂度:O(n^2)
---------------------------------------------------------具体代码+注释----------------------------------------------------------------
#include<bits/stdc++.h>
using namespace std;
int m[2000][2000]; //设置m数组,m[i][j]表示矩阵i~j的最小连乘次数
int s[2000][2000]; //记录最优断点,s[i][j]表示矩阵i~j的最优解断点位置
int p[2000]; //p[i-1]为矩阵i的行数;p[i]为Ai的列数
int n; //矩阵个数
int dp();
void print(int i,int j);
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>p[i];
cout<<dp()<<endl;
print(1,n);
return 0;
}
int dp(){
for(int i=n;i>=1;i--){ //行:自下而上
for(int j=i;j<=n;j++){ //列:对角线开始,从左到右
if(i==j) m[i][j]=0;
else{
m[i][j]=m[i+1][j]+ p[i-1] * p[i] * p[j]; //m[i][j]的初值为断点为i时的最少计算量
s[i][j]=i; //记录断点
for(int k=i+1;k<j;k++){ //尝试后续断点,寻找最优解
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;} //如果计算量更小则刷新m和s数组
}
}
}
}
return m[1][n];
}
void print(int i,int j){ //打印最优解方案
if(i==j) cout<<"A"<<i;
else{
int k=s[i][j];
cout<<"(";
print(i,k); //递归打印子问题中的矩阵和括号
print(k+1,j);
cout<<")";
}
3.问题总结:
无论是递归备忘录算法还是填表算法,核心思想都是先假设i为最初的断点,然后进行从i开始的第三重循环,寻找i~j的最佳断点
区别在于递归算法在寻找最佳断点时,对k处断点的计算次数采用的是递归调用i~k和k+1~j矩阵的子问题最优解
而填表法则是从下到上,从左到右的顺序已经填好了需要用到的元素,则每次计算计算量只需调用对应位置的数组值即可
三.状态转移方程


浙公网安备 33010602011771号