记忆化搜索
在前面我们已经学习了递归回溯等知识,什么是记忆化搜索,其实就是带着备忘录的递归,我们知道在递归过程中如果如果出现大量的重复的相同的子问题的时候,我们可能进行了多次递归,但是这些递归其实可以只用进行一次,我们只需要将结果保存起来,然后在进行递归之前先往备忘录里查看是否已经递归过了,如果是直接从备忘录里拿取结果,如果不是则进行递归。
注意:不是所有的递归都能使用记忆化搜索,只有遇到的子问题是一模一样的才能这样做!!!
实战演练
斐波那契额数
熟悉的题目,我们知道可以使用递归来做,但是大家最开始写的递归版本大概率时间复杂度是 O(2^n),这是因为递归次数太多了,并且出现很多重复的递归,这时候我们如果使用记忆化搜索,创建一个备忘录,在递归之前先看看备忘录里有没有这个数值,如果没有才进行递归,递归结束后顺便将结果填入备忘录。这样时间复杂度就被我们优化为 O(N),空间复杂度为 O(N)
class Solution {
//创建备忘录
int[] arr;
public int fib(int n) {
arr = new int[n + 1];
Arrays.fill(arr, -1);
return dfs(n);
}
int dfs(int n) {
if(n == 0 || n == 1) {
arr[n] = n;
return n;
}
//往备忘录里检查一下
if(arr[n] != -1) {
return arr[n];
}
int ret = dfs(n - 1) + dfs(n - 2);
arr[n] = ret;
return ret;
}
}
不同路径
我们知道机器人只能向右或者向下移动,要想知道到达目的地的路径数目,我们可以通过 达到 1 号地点的路径 数目 加上 2 号路径的数目的结果来获取。要想知道达到 1 号路径同样也要知道 1 号路径上面的 3 号路径的数目和左边 4 号路径的数目。好了,相同的一模一样的子问题出来了,我们可以使用记忆化搜索了。
从上面的分析,我们知道我们要使用逆向思维了,我们的递归开始地点应该从 目的地开始。
然后递归上面 和 左边也就是说 dfs(x, y) = dfs(x-1, y) + dfs(x, y-1)
我们赋予递归一个牛逼的使命,递归的任务直接返回一共有多少条路径数目。
递归的出口:dfs(x-1, y) + dfs(x, y-1) 这条式子获得的新的 x 和 y 都可能为负数,也就是会出现越界访问的情况,这就是递归的出口。
记忆化的处理:递归之前先检查备忘录,递归之后的结果同时保存到备忘录里。
class Solution {
//创建备忘录
int[][] memo;
public int uniquePaths(int m, int n) {
memo = new int[m][n];
return dfs(m - 1, n - 1);
}
int dfs(int x, int y) {
//避免越界
if(x == -1 || y == -1) {
return 0;
}
if(memo[x][y] != 0) {
return memo[x][y];
}
if(x == 0 && y == 0) {
memo[x][y] = 1;
return 1;
}
memo[x][y] = dfs(x-1 ,y) + dfs(x, y-1);
return memo[x][y];
}
}
最长递增子序列
,
我们会遍历数组每一个元素,然后以每一个元素为基础进行递归搜索看看最长递增子序列的长度。
我们用备忘录来保存每一个元素所处的最大递增子序列。
class Solution {
//创建备忘录
int[] memo;
int n;
public int lengthOfLIS(int[] nums) {
n = nums.length;
memo = new int[n];
int sum = 0;
for(int i = 0; i < n; i++) {
sum = Math.max(dfs(nums, i), sum);
}
return sum;
}
int dfs(int[] nums, int str) {
if(memo[str] != 0) {
return memo[str];
}
int sum = 1;
for(int i = str + 1; i < n; i++) {
if(nums[i] > nums[str]) {
sum = Math.max(sum, dfs(nums, i) + 1);
}
}
memo[str] = sum;
return memo[str];
}
}
猜数字大小 Ⅱ
题目解析:猜数字的范围 是 1 ~ n,我们要从中找到最小花费,也就是从哪个数字开始猜是最小的花费,当我们开始从这个数字开始猜的时候,我们要找到这里面最大的花费,才能保证我们从这个数字开始猜一定够钱猜。
从中我们知道我们要先遍历 1 ~ n 找到最小花费,然后从某个数字开始猜的时候,有两种可能,一个是猜大了,另一个是猜小了,然后从这两种情况找到最大花费的情况。
我们定义一个递归函数,赋予其一个使命:从 1 ~ n 猜返回最小的花费,我们使用一个二维备忘录来记录数据,首先,每次猜数字都有两种情况(猜大了或者猜小了)这时候又要进行大范围或者小范围的递归搜索,所以使用二维数组进行保存结果。
class Solution {
//创建备忘录
int[][] memo;
int n;
public int getMoneyAmount(int _n) {
n = _n;
memo = new int[n + 1][n + 1];
return dfs(1, n);
}
int dfs(int left, int right) {
if(left >= right) {
return 0;
}
if(memo[left][right] != 0) {
return memo[left][right];
}
int ret = Integer.MAX_VALUE;
for(int head = left; head <= right; head++) {
int x = dfs(left, head - 1);//猜小了
int y = dfs(head + 1, right);//猜大了
ret = Math.min(Math.max(x, y) + head, ret);
}
memo[left][right] = ret;
return ret;
}
}
矩阵中的最长递增路径
这个题目就是之前我们使用的 FloodFill 算法,只不过这里使用了备忘录进行了优化。
class Solution {
//创建备忘录
int[][] memo;
int[] dx = {0, 0, 1, -1};
int[] dy = {1, -1, 0, 0};
int m, n;
public int longestIncreasingPath(int[][] matrix) {
m = matrix.length;
n = matrix[0].length;
memo = new int[m][n];
int sum = 0;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
sum = Math.max(dfs(matrix, i, j), sum);
}
}
return sum;
}
int dfs(int[][] matrix, int i, int j) {
if(memo[i][j] != 0) {
return memo[i][j];
}
int path = 1;
for(int k = 0; k < 4; k++) {
int x = dx[k] + i;
int y = dy[k] + j;
if(x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
path = Math.max(path, dfs(matrix, x, y) + 1);
}
}
memo[i][j] = path;
return path;
}
}