动态规划
基本思想:
- 动态规划是一种多阶段决策过程最优的通用方法.
- 动态规划算法与分治法类似, 其思想把求解的问题分成许多阶段或多个子问题, 然后按顺序求解各子问题. 最后一个阶段或子问题的解就是初始问题的解.
- 动态规划中分解得到的子问题往往不是相互独立的. 但不同子问题的数目常常只有多项式级. 用分治法求解时, 有些子问题被重复计算了许多次, 从而导致分治法求解问题时间复杂度较高
- 动态规划基本思想是保留已解决的子问题的解,在需要时再查找已求得的解,就可以避免大量重复计算,进而提升算法效率.
适用条件:
- 求解的问题是组合优化问题.
- 求解的过程需要多步判断, 从小到大依次求解.
- 子问题目标函数最优解之间存在依赖关系(原问题最优解是由子问题最优解构成)
ex: 多起点, 多终点的最短路径
- 每个节点到下一层节点的最短路径之和, 即为起点->终点的最短路径
- 求解过程中, 需要每到一个节点判断一次最近节点信息.
- 以上满足动态规划条件
基本步骤:
(1)找出最优解的性质,并刻画其结构特征。
(2)递推地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
要素
最优子结构
重叠子问题
备忘录(表格)
例题1:
求解矩阵连乘问题, 首先复习一下线性代数中矩阵乘法的相关知识.
给定矩阵 a[n][m] b[m][k] 两矩阵相乘
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
//c矩阵的第i行第j列所对应的数值,等于a矩阵的第i行分别乘以b矩阵的第j列之和
for (int t = 0; t < k; k++)
c[i][j] += a[i][k] * b[k][j];
由上易得, 每当进行一次矩阵乘法, 就需要进行 nmk 次基本乘法操作.
现在回到问题中, 在矩阵连乘的情况下, 不同的乘法次序会对运算的复杂度造成影响吗?
现在可以确定, 通过优化矩阵连乘的次序, 可以大量减少运算次数.
而且我们会发现一个规律, 对于任何断开处, 总有最后一次运算是:
最前方矩阵行数 断开处矩阵列数 结尾处矩阵列数**
这一点在之后递归非常重要
我们现在需要寻找合适的"断点", 也就是从哪个位置后, 优先进行乘法运算使得最终的运算次数最少.
就如上述 A_1 A_2 A_3 三个矩阵连乘, 从A_1后断开, 先算A_2A_3, 运算次数大于从0处断开先运算A_1A_2.
我们先尝试用蛮力法求解矩阵连乘问题的最优次序情况.
蛮力法:
搜索所有可能的计算次序,并计算出每种计算次
序相应需要的数乘次数,从中找出一种数乘次数最少的计
算次序。设不同计算次序为P(n)
指数级时间复杂度, 反而影响了算法效率
我们可以重新审视一下问题
说明:
将矩阵连乘积AiAi+1…Aj简记为A[i:j],i≤j。
考察计算A[i:j]的最优计算次序。设这个计算次序在矩阵
Ak和Ak+1之间将矩阵链断开,i≤k<j,则其相应完全
加括号方式为(AiAi+1…AK)(Ak+1Ak+2…Aj)
计算量:
A[i:k]的计算量加上A[k+1:j]的计算量,再加上
A[i:k]和A[k+1:j]相乘的计算量。
进一步分析, 如果(AiAi+1…AK)(Ak+1Ak+2…Aj)要得到最优, 两数相乘得到最小数的情况, 即为两数都是问题范围内最小值. 满足动态规划的最优结构.
所以我们使用动态规划进行求解
1.建立递推关系
(1)设计算A[i:j],1≤i≤j≤n,所需要的最少数乘次数m[i,j],则
原问题的最优值为m[1,n].
(2)递推方程
k只有j-i种可能的位置
参数说明:
p: 建立一个序列p, 存储连乘序列中矩阵的行列值. 因为前一个矩阵的列与后一个矩阵的行是同一个值, 该连乘序列才有效(才能构成矩阵乘法操作).
所以p0 = A1矩阵的行数量, p1 = A1矩阵的列数量, A2矩阵的行数量.
综上, p的维度要比连乘序列大小(矩阵个数)大 1.
s: 存储连乘序列断开的位置
递归实现
/**
* 递归实现矩阵连乘最优位置, 寻找最少乘次
* @param i 连乘序列的起始位置, i>0(序列从1开始)
* @param j 连乘序列的结束位置, j<n+1(不存在第n+1个)
* @param p 连乘矩阵序列行列值
* @param s 存储矩阵链最少乘次
* @return 当前矩阵链的最小乘次
*/
int RecurMatrixChain(int i, int j, int[] p, int[][] s){
//当传入的矩阵链长度只有1时, 最小乘次为0
if(i == j){
return 0;
}
/**
* 初始化最小乘次值 u
* 当从i处断开时, 将子序列 i -> i, i+1 -> j 继续进行求最小乘次处理
* 连乘序列中p[i-1](起始位置的行数), p[k](断开位置的列数), p[j](序列末尾的列数目) 相乘
* 上述乘积, 保存了对于当前断开点位来说所需的乘次数目
* 对于任何断开处, 总有最后一次运算是:
* 最前方矩阵行数* 断开处矩阵列数* 结尾处矩阵列数
*/
int u = RecurMatrixChain(i,i,p,s) + RecurMatrixChain(i+1,j,p,s) + p[i-1]*p[i]*p[j];
s[i][j] = i;
for(int k = i+1; k < j; k++){
int t = RecurMatrixChain(i, k,p,s) + RecurMatrixChain(k+1, j, p, s) + p[i-1]*p[k]*p[j];
if(t < u){
u = t;
s[i][j] = k;
}
}
return u;
}
我们只需要建立一个矩阵, 保存当前矩阵链的子序列断开时, 子序列各自的最小乘次, 算到更大的序列时, 只需要调用子序列的值进行加和操作.
操作步骤:
- 从对角线开始, 向上三角依次求解. 求出的是, 针对当前断开位置
最前方矩阵行数* 断开处矩阵列数* 结尾处矩阵列数 + 断开位置前方序列的最小乘次 + 断开位置后方的最小乘次. - 保存对应位置的断开位置
/**
*
* @param p 连乘矩阵行列值
* @param n 连乘矩阵的总数目
* @param m 连乘矩阵的最少乘次
* @param s 连乘矩阵的断开位置
*/
void MatrixChain(int[] p, int n, int[][] m, int[][] s){
//初始化矩阵
for(int i = 1; i <= n; i++){
m[i][i] = 0;
}
//进行最少乘次矩阵运算
for(int r = 2; r <= n; r++){//从序列开始位置进行运算
for(int i = 1; i <= n-r+1; i++){//向后依次遍历
int j = i+r-1;
m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];
s[i][j] = j;
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;
}
}
}
}
}
------------------------------THE END----------------------------