记忆化搜索
在递归中,我们往往进行了大量的回溯与搜索(或者是理解为一种嵌套的调用操作)这些操作总会造成大量的重复计算,想一想,如果在进行递归时找到了某个以前算过的值,却还要继续重新递归一遍,这样是不是很费时间呢?答案是显然的,如果有什么办法能够把以前算个的值记录下来就好了。
记忆化搜索就很好地解决了这个问题
看一个例子:
1.1斐波那契的记忆化:
在动态规划思想中,我们有一个重要的必要条件:“无后效性”,也就是说,当前的过程与以前的无关,比如在傍晚5点时,你思考晚上准备吃什么,这个时候你只会关注到中午吃的多不多,而不会关注早上吃的多不多,换言之,虽然早上吃的多不多这个结果影响了中午的饭量而间接影响了晚上的饭量,但是在晚饭的时候你并不需要考虑早上吃的多不多,只需要考虑中午,因为早上造成的结果对中午造成了影响,中午的结果已经“定”了(或者理解为被储存了)于是晚上吃多少与以前(早上)无关,而与其相邻的状态(中午)有关,所以满足无后效性。
例子:我们知道,斐波那契的递推式可以这么写:
int fibo(int n)
{
if(n==1 || n==2)
return 1;
return fibo(n - 1) + fibo(n - 2);
}
实际上,如果这么写,会有很多重复计算,比如n == 3时,在计算4的时候需要fibo(3),在计算5的时候也要fibo(3),更不用说当数据到达50甚至更多,这要的效率到达了o( 2 n {2}^{n} 2n),慢的一批。
通过记忆化搜索,我们可以实现效率指数型降低:
int memory[N] //记忆数组
int fibo(n)
{
if(n == 1 || n == 2)return 1;
if(memory[n] != 0)
return memory[n] // 直接将结果返回(看不懂的小伙伴往回溯方向想,如果该处无值就往下走,如果有值就返回,这就是一种记忆的过程
memory[n] = fibo(n - 1) + fibo(n - 2);
return memory[n];
}
以上记忆化搜索时间复杂度是o(n),但是开辟了数组,也就是说,用空间复杂度换了一点时间复杂度。
1.2数塔问题
【动规:递归求解】
个人代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a[1000][1000] = { 0 };
int i, j;
int n;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
for (j = 0; j < i; j++)
{
scanf("%d", &a[i][j]);
}
}
int dp[1000][1000] = { 0 };
for (i = 1; i <= n; i++)
{
for (j = 0; j < i; j++)
{
if (i == 1 && j == 0)
dp[i][j] = a[i][j];
else if (j == 0)
{
dp[i][j] = dp[i - 1][j] + a[i - 1][j];
}
else if (j == n - 1)
{
dp[i][j] = dp[i - 1][j - 1] + a[i - 1][j - 1];
}
else
{
dp[i][j] = max((dp[i - 1][j] + a[i - 1][j]), (dp[i - 1][j - 1] + a[i - 1][j - 1]));
}
}
}
int max = 0;
int* p = &dp[n][0];
for (i = 0; i < n; i++)
{
if (max < *p)
{
max = *p;
}
p++;
}
printf("%d", max);
return 0;
}
上面虽然实现但是效率实际上很低,没有使用递归.(我使用的是dp不是dfs使用的空间复杂度高)因为每个都要计算,对比下面的可看出其的确简洁很多。
优化:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int d[N][N];
int n;
int ans = -1e9;
// 求从点(x,y)开始,走到最后一层,经过数字的最大和。
int fun(int x, int y)
{
if(x == n) return a[x][y];// 最后一层的解就是自己
return a[x][y] + max(fun(x + 1, y), fun(x + 1, y + 1));// 如果不是最后一行就递归计算
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
cout << fun(1, 1);
return 0;
}
使用了递归,快了点,但是不够,因为还是存在大量重复计算。
动规:记忆化搜索:
- 求解每一个点的值,先判断该点的值是否曾经求解过,如果曾经求解过,直接拿过来使用;如果没求解过,递归求解,并存储该解!
- 将计算过的值存储到一个数组中
- 如何判断是否求解过呢?——做标记判断
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int d[N][N];
int n;
int ans = -1e9;
//动规,记忆化搜索:先将d数组初始化为-1,方便判断有没有求解过
int fun(int x, int y)
{
if(x == n) return a[x][y];// 最后一层的解就是自己
else
{
if(d[x][y] != -1) return d[x][y];// 曾经求解过
else
{
//求解(x,y)点走到底层经过的数字和的最大值,并存储
d[x][y] = a[x][y] + max(fun(x + 1, y), fun(x + 1, y + 1));
return d[x][y];
}
}
}//这是反向的,也可以自己尝试一下正向.
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
memset(d, -1, sizeof d);
cout << fun(1, 1);
return 0;
}
总结
记忆化深搜,其实就是对递归dfs的一种优化,将计算过的记录下来,避免重复计算。记忆化深搜也属于DP的一种!