一.记忆化搜索概述
1.概念
搜索是一种简单有效但是效率又很低下的算法结构,其低效的原因主要在于存在很多重叠子问题。而记忆化搜索则是在搜索的基础上,利用数组来记录已经计算出来的重叠子问题状态,进行合理化的剪枝,从而降低时间复杂度。这个记录状态的过程就是记忆化的过程,我们需要找到不同搜索层次之间的子问题、状态转移关系,这与动态规划的思想又不谋而合。
简单来说,记忆化搜索是一种典型的空间换时间的思想,记忆化搜索 = 深度优先搜索实现 + 动态规划思想(记录状态、剪枝)。
2.图示
此处以某个记忆化搜索题目的结构树为例。在搜索过程中,我们将问题的搜索树画出来如下所示。左侧为暴力搜索的搜索树,而右侧为记忆化搜索剪枝的搜索树。
记忆化搜索可以看作是动态规划的前置过程,或者说记忆化搜索一般是自顶向下的,而动态规划一般是自底向上的。
二.例题
1.LeetCode 45. 跳跃游戏改
给定一个长度为 n 的整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处(0 <= j <= nums[i])
返回到达
nums[n - 1]
的最小跳跃次数。已知跳跃次数有上限K,若无法在K次内到达则返回 -1
(1)记忆化搜索
对于该题,我们可以先求出到达终点的最小跳跃次数 t,然后比较 t 与上限 K 的大小,若t > K则返回 -1 。
按照搜索的思想来说,我们在到达某个索引 i 时,该位置到达终点的最小跳跃次数取决于 min(dfs(i) , dfs(i+j) + 1),0 <= j <= nums[i] ;按照记忆化的思想,像最短路一样,某个位置 i 到达终点的最小跳跃次数应该是确定的,属于重叠子问题,不应该重复搜索,因此可以使用数组记录每个位置的最小次数。
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10000 + 7;
int n,k,nums[maxn],mem[maxn];
int dfs(int index){
if(index >= n-1)return 0;
if(mem[index]!=-1)return mem[index];//记忆化
int ans = -1;
for(int i = 1;i<=nums[index];i++){
int res = dfs(index+i)+1;
if(res != 0){
ans = ans==-1?res:min(ans,res);
}
}
return mem[index] = ans;
}
int main()
{
memset(mem,-1,sizeof(mem));
scanf("%d%d",&n,&k);
for(int i = 0;i<n;i++){
scanf("%d",&nums[i]);
}
dfs(0);
if(mem[0] == -1 || mem[0] > k)printf("-1\n");
else printf("%d\n",mem[0]);
}
(2)贪心
记忆化搜索的方式可行,但是复杂度还是有点高会超时。我们重新来审视这个题,每个位置处的跳跃距离是固定的、相互独立的,与之前的子节点和之后的子节点都是没关系的,也就是说该题其实与动态规划思想无关。
考虑现在跳到了位置 i ,那么接下来应该选择跳到 i+1 ~ i+nums[i] 的哪个位置呢?答案是「贪心」地选择能跳跃到距离最后一个位置最远的那个位置(即使得“探索序列”能够拓宽最远的那个位置),原因是以该位置为下一次起跳点所能到达的地方,由于其是最远的,所以能够覆盖其他所有的起跳点位置范围,这肯定是最优的。
当一次 跳跃 结束时,从下一个格子开始,到现在 能跳到最远的距离,都 是下一次 跳跃 的 起跳点。所以跳完一次之后,不断更新维护下一次 起跳点的范围。在新的范围内跳,更新 能跳到最远的距离。
int jump(int len){
int ans = 0;
int start = 0,last = 0; //初始起跳范围 [0,0] 闭区间
while(last < len - 1){
int maxpos = 0;
//选择下一次跳哪个
for(int i = start;i<=last;i++){
maxpos = max(maxpos,i+nums[i]);
}
start = last+1; // 下一次起跳点范围开始的格子
last = maxpos; // 下一次起跳点范围结束的格子
ans++; // 跳跃次数
}
return ans;
}