算法沉淀——记忆化搜索
- 01.斐波那契数
- 02.不同路径
- 03.最长递增子序列
- 04.猜数字大小 II
- 05.矩阵中的最长递增路径
记忆化搜索算法(Memoization)是一种通过存储已经计算过的结果来避免重复计算的优化技术,通常应用于递归算法中。这种技术旨在提高算法的效率,减少重复计算,特别是对于具有重叠子问题的问题。
主要特点和应用场景包括:
- 避免重复计算: 记忆化搜索算法通过缓存已经计算过的结果,以避免对相同输入进行重复计算。这在递归算法中特别有用,因为递归往往会导致相同的子问题被反复解决。
- 提高效率: 通过保存中间计算结果,记忆化搜索算法能够大幅减少算法的时间复杂度,从指数级别降低到多项式级别。
- 动态规划: 记忆化搜索在动态规划中经常被使用。动态规划是一种解决优化问题的方法,通常包含递归和子问题重叠的特点。记忆化搜索能够避免重复计算,使得动态规划算法更加高效。
- 递归算法优化: 记忆化搜索主要用于优化递归算法。在递归调用中,如果存在相同的输入参数,记忆化搜索算法将直接返回已经计算过的结果,而不是重新执行计算。
- 应用于搜索问题: 记忆化搜索不仅用于动态规划,还可以应用于搜索问题,特别是深度优先搜索中的状态记忆。
经典的例子包括斐波那契数列的递归实现、图的最短路径问题中的递归搜索等。在实现记忆化搜索时,通常需要使用数据结构(如哈希表、数组等)来保存已经计算过的结果。
记忆化搜索和动态规划都是一种用于优化递归算法的技术,它们有很多相似之处,但也存在一些关键的区别。下面是它们之间的相同之处和不同之处
相同之处:
- 重叠子问题: 记忆化搜索和动态规划都针对具有重叠子问题性质的问题。这意味着问题可以被划分为许多相似的子问题,这些子问题在解决整体问题时会被多次重复计算。
- 优化递归: 两者都旨在优化递归算法。递归算法通常会导致相同的子问题被反复解决,而记忆化搜索和动态规划都致力于避免这种重复计算。
- 缓存中间结果: 记忆化搜索和动态规划都使用某种形式的缓存来存储已经计算过的中间结果,以便在需要时直接返回结果,而不是重新计算。
不同之处:
- 自顶向下 vs 自底向上: 记忆化搜索是自顶向下的方法,从大问题开始,逐步分解为子问题,并缓存这些子问题的结果。动态规划是自底向上的方法,从最小的子问题开始解决,逐步构建出整个问题的解。
- 递归 vs 迭代: 记忆化搜索通常使用递归实现,通过递归调用来解决问题。动态规划则使用迭代的方式,通过循环来计算并填充表格或数组。
- 状态转移方程 vs 递归调用: 在动态规划中,通常通过状态转移方程来描述问题的子问题之间的关系,从而构建解。而在记忆化搜索中,通常直接使用递归调用来表示问题的分解。
- 使用场景: 记忆化搜索通常更适用于处理问题的子问题规模较小,而动态规划则更适用于处理问题的子问题规模较大的情况。
总的来说,记忆化搜索和动态规划都是用于优化递归算法的技术,但它们的实现方式和问题解决的思路有一些不同。在解决问题时,根据具体的情况选择使用记忆化搜索或动态规划能够更好地满足问题的需求。
01.斐波那契数
题目链接:https://leetcode.cn/problems/fibonacci-number/
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n
,请计算 F(n)
。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
思路
这里我们使用记忆化搜索算法,存放已经计算的斐波那契数,可以使递归算法达到线性级别,同样动态规划算法也可以,只不过该算法思维略有不同,其实算法是几乎一致的。
代码
class Solution {
vector<int> memo;
public:
Solution():memo(31,-1){};
int fib(int n) {
if(memo[n]!=-1) return memo[n];
if(n==0||n==1)
{
memo[n]=n;
return n;
}
memo[n]=fib(n-1)+fib(n-2);
return memo[n];
}
};
或
class Solution {
int dp[31];
public:
int fib(int n) {
dp[0]=0,dp[1]=1;
for(int i=2;i<=n;++i)
dp[i]=dp[i-1]+dp[i-2];
return dp[n];
}
};
02.不同路径
题目链接:https://leetcode.cn/problems/unique-paths/
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 * 109
思路
同样这里如果使用普通的递归深度遍历,会有大量重复计算导致时间复杂度过高,所以这里要使用记忆化搜索来辅助递归算法,达到线性的时间复杂度,我们计算每个格子的路径,相当于上面格子的路径加上左边格子的路径一直递推,完成递归的逆推,同样动态规划也是按照这个逻辑顺推过去。
代码
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> memo(m+1,vector<int>(n+1,0));
return dfs(m,n,memo);
}
int dfs(int m,int n,vector<vector<int>>& memo){
if(memo[m][n]!=0) return memo[m][n];
if(m==0||n==0) return 0;
if(m==1&&n==1){
memo[m][n]=1;
return 1;
}
memo[m][n]=dfs(m-1,n,memo)+dfs(m,n-1,memo);
return memo[m][n];
}
};
或
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
dp[1][1]=1;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(i==1&&j==1) continue;
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m][n];
}
};
03.最长递增子序列
题目链接:https://leetcode.cn/problems/longest-increasing-subsequence/
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
思路
深度遍历每个位置向后推最长的递增序列,加上备忘录记忆化搜索做优化,同样动态规划就是在该思想上有后往前递推即可。
代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> memo(n,0);
int ret=0;
for(int i=0;i<n;++i)
ret=max(ret,dfs(i,nums,memo));
return ret;
}
int dfs(int pos,vector<int>& nums,vector<int>& memo){
if(memo[pos]!=0) return memo[pos];
int ret=1;
for(int i=pos+1;i<nums.size();i++){
if(nums[i]>nums[pos])
ret=max(ret,dfs(i,nums,memo)+1);
}
memo[pos]=ret;
return ret;
}
};
或
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n,1);
int ret=0;
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(nums[j]>nums[i])
dp[i]=max(dp[i],dp[j]+1);
}
ret=max(ret,dp[i]);
}
return ret;
}
};
04.猜数字大小 II
题目链接:https://leetcode.cn/problems/guess-number-higher-or-lower-ii/
我们正在玩一个猜数游戏,游戏规则如下:
- 我从
1
到n
之间选择一个数字。 - 你来猜我选了哪个数字。
- 如果你猜到正确的数字,就会 赢得游戏 。
- 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
- 每当你猜了数字
x
并且猜错了的时候,你需要支付金额为x
的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n
,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
- 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
- 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
示例 2:
输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
示例 3:
输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。
思路
这里我们最简单的办法就是使用dfs暴力搜索每个猜数字的情况,在左右子树中找到较小的数累加,完成比对,加上记忆化搜索即可。
代码
class Solution {
int memo[201][201];
public:
int getMoneyAmount(int n) {
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=INT_MAX;
for(int mid=left;mid<=right;mid++){
int x=dfs(left,mid-1);
int y=dfs(mid+1,right);
ret=min(ret,mid+max(x,y));
}
memo[left][right]=ret;
return ret;
}
};
05.矩阵中的最长递增路径
题目链接:https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/
给定一个 m x n
整数矩阵 matrix
,找出其中 最长递增路径 的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外(即不允许环绕)。
示例 1:
输入:matrix = [[9,9,4],[6,6,8],[2,1,1]]
输出:4
解释:最长递增路径为 [1, 2, 6, 9]。
示例 2:
输入:matrix = [[3,4,5],[3,2,6],[2,2,1]]
输出:4
解释:最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。
示例 3:
输入:matrix = [[1]]
输出:1
提示:
m == matrix.length
n == matrix[i].length
1 <= m, n <= 200
0 <= matrix[i][j] <= 231 - 1
思路
这里通过简单的深度优先遍历思路是没有问题的,但在时间上达不到要求,所以这里要加上记忆化搜索来进行优化。
代码
class Solution {
const int dx[4]={0,0,1,-1};
const int dy[4]={1,-1,0,0};
int m,n;
int memo[201][201];
public:
int longestIncreasingPath(vector<vector<int>>& matrix) {
m=matrix.size(),n=matrix[0].size();
int ret=0;
for(int i=0;i<m;++i){
for(int j=0;j<n;++j){
ret=max(ret,dfs(matrix,i,j));
}
}
return ret;
}
int dfs(vector<vector<int>>& matrix,int i,int j){
if(memo[i][j]!=0) return memo[i][j];
int ret=1;
for(int k=0;k<4;k++){
int x=i+dx[k],y=j+dy[k];
if(x>=0&&x<m&&y>=0&&y<n&&matrix[x][y]>matrix[i][j]){
ret=max(ret,dfs(matrix,x,y)+1);
}
}
memo[i][j]=ret;
return ret;
}
};