为什么称之为记忆化搜索?动态规划(Dynamic Programming,简称DP)通常也被称为记忆化搜索,主要是因为它结合了动态规划的基本原理和回溯算法中的记忆化技术。
动态规划使用空间换取时间,也有人称之为带备忘录的递归或者叫递归树的“剪枝”,都是同一个意思,因为我们不需要对同一个树子节点进行重复计算。有了递归我们也可以将它改成非递归(迭代)的形式。
穷举法/暴力搜索→记忆化搜索/剪枝→改成迭代形式
动态规划(Dynamic Programming)
动态规划是一种解决问题的方法,它将复杂问题分解为更简单的子问题,并利用这些子问题的解来构建原始问题的解。动态规划通常用于解决具有重叠子问题和最优子结构特性的问题。其核心思想是:
- 重叠子问题:问题的解决方案可以通过解决更小的子问题来构建,而这些子问题会被多次重复解决。
- 最优子结构:问题的最优解包含其子问题的最优解。
动态规划通过存储子问题的解(通常是在表格中)来避免重复计算,从而提高效率。
记忆化搜索(Memoization)
记忆化是回溯算法中的一种技术,用于存储已经计算过的子问题的结果,以便在后续的递归调用中直接使用这些结果,避免重复计算。记忆化通常用于解决具有以下特性的问题:
- 重复计算:相同的子问题被多次计算。
- 递归结构:问题通过递归分解为更小的子问题。
记忆化技术通过缓存子问题的解来减少计算量。
动态规划与记忆化搜索的关系
动态规划和记忆化搜索之间的联系在于它们都试图通过存储子问题的解来避免重复计算。不同之处在于:
- 动态规划:通常是自底向上的,从最小的子问题开始,逐步构建到原始问题的解。它通常使用表格(如数组或矩阵)来存储中间结果。
- 记忆化搜索:通常是自顶向下的,从原始问题开始,递归地分解为子问题,并在递归过程中存储中间结果。它通常使用哈希表或其他数据结构来缓存结果。
在某些情况下,动态规划和记忆化搜索可以视为等价的,因为它们都能有效地解决具有重叠子问题的问题。这就是为什么动态规划有时也被称为记忆化搜索的原因。实际上,记忆化搜索可以看作是一种优化过的递归,而动态规划可以看作是一种迭代的记忆化搜索。
动态规划步骤:
动态规划算法适用于解最优化问题,通常可以按照以下步骤设计动态规划算法:
(1)找出最优解的性质,并刻画其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
矩阵连乘问题
上面是b站上的一个视频讲解矩阵连乘的逻辑的,不能理解这个题的同学可以去看一下。
完整代码:
public class HelloWorld {
// 定义一个数组p,存储矩阵的维度,p[i]表示第i个矩阵的行数,因为矩阵是方阵,所以也是列数。
public static int p[] = {30, 35, 15, 5, 10, 20, 25};
// 定义一个二维数组s,用于存储矩阵连乘的最优分割点。
public static int[][] s = new int[10][10];
// 定义一个二维数组m,用于存储矩阵连乘的最小计算次数(备忘录)。
public static int[][] m = new int[10][10];
// 私有静态方法,用于计算矩阵链乘的最优解。
private static int memorizedMatrixChain(int n) {
// 初始化备忘录数组m。
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
m[i][j] = 0;
}
}
// 调用lookupChain方法计算并返回最优解。
return lookupChain(1, n);
}
// 静态方法,用于递归查找矩阵链乘的最优解。
public static int lookupChain(int i, int j) {
// 如果备忘录中已经有值,则直接返回。
if (m[i][j] > 0) return m[i][j];
// 如果只有一个矩阵,不需要乘法,返回0。
if (i == j) return 0;
// 初始化乘法次数为最大值。
int u = lookupChain(i + 1, j) + p[i - 1] * p[i] * p[j];
s[i][j] = i;
// 寻找最优分割点。
for (int k = i + 1; k < j; k++) {
int t = lookupChain(i, k) + lookupChain(k + 1, j) + p[i - 1] * p[k] * p[j];
// 如果当前分割点的乘法次数更少,则更新最优解。
if (t < u) {
u = t;
s[i][j] = k;
}
}
// 将最优解存入备忘录。
m[i][j] = u;
return u;
}
// 静态方法,用于输出矩阵连乘的最优分割顺序。
public static void traceback(int[][] s, int i, int j) {
// 如果只有一个矩阵或两个矩阵,不需要进一步分割。
if (i == j || j - i == 1) return;
// 输出当前分割点的分割信息。
System.out.println("矩阵A" + s[i][j] + "和矩阵A" + (s[i][j] + 1) + "之间分段。");
// 递归输出i到s[i][j]的分割顺序。
traceback(s, i, s[i][j]);
// 递归输出s[i][j]+1到j的分割顺序。
traceback(s, s[i][j] + 1, j);
}
public static void main(String[] args) {
int minCost = memorizedMatrixChain(6); // 计算最小成本
System.out.println("最优矩阵连乘积的次数为: " + minCost);
traceback(s, 1, 6); // 输出最优分割顺序
}
}
伪代码的形式:
// 初始化矩阵维度数组p,存储矩阵的行数和列数
p = [30, 35, 15, 5, 10, 20, 25]// 初始化备忘录数组m和分割点数组s
m = 二维数组(10x10) 初始化为0
s = 二维数组(10x10) 初始化为0// 计算矩阵链乘的最优解
function memorizedMatrixChain(n)
for i from 1 to n
for j from i to n
m[i][j] = 0
return lookupChain(1, n)// 查找矩阵链乘的最优解
function lookupChain(i, j)
if m[i][j] > 0 then
return m[i][j]
if i == j then
return 0
u = lookupChain(i+1, j) + p[i-1] * p[i] * p[j]
s[i][j] = i
for k from i+1 to j-1
t = lookupChain(i, k) + lookupChain(k+1, j) + p[i-1] * p[k] * p[j]
if t < u then
u = t
s[i][j] = k
m[i][j] = u
return u// 输出矩阵链乘的最优分割顺序
function traceback(s, i, j)
if i == j or j-i == 1 then
return
print "矩阵A" + s[i][j] + "和矩阵A" + (s[i][j] + 1) + "之间分段。"
traceback(s, i, s[i][j])
traceback(s, s[i][j] + 1, j)// 主程序
n = p数组的长度 - 1
minCost = memorizedMatrixChain(n)
print "最优矩阵连乘积的次数为: " + minCost
traceback(s, 1, n)
我们可以分析其时间复杂度如下:
时间复杂度分析:时间复杂度是O()。
空间复杂度:空间复杂度是O()。