前序介绍:
首先,在进行暴搜时,我们主要历经了以下几个阶段:循环--递推--递归--dp(动态内存管理)--记忆化搜索--矩阵快速幂
记忆化搜索:相当于在递归中进行一次剪枝,并且这个剪枝可以大大优化我们的算法,可以形象地将其称为 “带着备忘录的递归”
实现方法:
1:添加一个备忘录(<可变参数,返回值>)(一般情况,要将其进行初始化)
2::递归每次返回的时候,要将值存放到备忘录中
3:在每次开始递归时,要首先去备忘录里面查找
动态规划和记忆化搜索存在着一一对应的关系:
(1)确定状态表示----dfs函数的含义
(2)推导状态转移的方程--dfs函数的函数体
(3)初始化--dfs函数递归出口
(4)确定填表顺序--填写备忘录的顺序
(5)确定返回值--主函数如何调用dfs
二者本质:
(1):暴力搜索
(2):对暴搜的优化,把已经计算过的值存放起来
记忆化搜索的一些特性:
(1)并不是所有的递归(暴搜,深搜)都可以改成记忆化搜索,需要满足在递归过程中,出现了大量相同问题的这个条件
(2)带着备忘录的递归,记忆化搜索,动态内存管理,这三者本质上并没有任何区别
(3)通过暴搜-->记忆化搜索这种实现代码的方法,和普通的动态内存管理的方法,各自有各自的优点,要根据具体问题具体分析
一:斐波那契数(重点)
题目:
斐波那契数(通常用F(n)表示)形成的序列称为斐波那契数列。该数列由0和1开始,后面的每一项数字都是前面两项数字的和,也就是:
F(0) = 0,F(1) = 1,F(n) = F(n-1)+ F(n-2);
给定n,计算F(n)
方法:
解法一:
直接利用递归进行求解可以得到最终答案,但是时间复杂度过大会超时
解法二:
记忆化搜索,添加一个备忘录,将代码算法进行优化
class Solution {
int memo[100];
public:
int fib(int n) {
memset(memo,-1,sizeof memo);//将备忘录进行初始化,这种初始化方法不要忘记哈
return dfs(n);
}
int dfs(int n)
{
//在递归开始看备忘录里是否已经存放了该值
if(memo[n]!=-1)
{
return memo[n];
}
if(n==0||n==1)
{
memo[n] = n;
return n;
}
memo[n] = dfs(n-1)+dfs(n-2);
return dfs(n-1)+dfs(n-2);
}
};
解法三:
利用动态内存管理进行解决
class Solution {
int memo[100];
public:
int fib(int n) {
memset(memo,-1,sizeof memo);//将备忘录进行初始化,这种初始化方法不要忘记哈
for(int i = 0;i<=n;i++)
{
if(i==0||i==1) memo[i] = i;
else memo[i] = memo[i-1]+memo[i-2];
}
return memo[n];
}
};
二:不同路径
题目:
一个机器人位于一个m*n网格的左上角。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角,问总共有多少条不同的路径?
方法:
首先,先利用暴搜找到利用递归的代码思路,然后展开决策树,看是否可以将其化为记忆化搜索,接着将其化为动态内存管理
题目分析可有:
要得到到五角星位置的路径等于其左边的叉的路径再加上其上边的叉的路径,即:
dfs(2,4) = dfs(1,4) + dfs(2,3)
令有dfs函数,传给其位置,可以得到起点到该位置的所有路径数
int dfs(int i,int j);
递归出口:
i==1&&j==1 return 1;
i==0||j==0 return 0;
记忆化搜索带代码:
class Solution {
int memo[101][101];
public:
int uniquePaths(int m, int n) {
memset(memo,-1,sizeof memo);//初始化备忘录
return dfs(m,n);
}
int dfs(int i,int j)
{
if(memo[i][j]!=-1)
{
return memo[i][j];
}
if(i==1&&j==1)
{
memo[i][j] = 1;
return 1;
}
if(i==0||j==0)
{
memo[i][j] = 0;
return 0;
}
memo[i][j] = dfs(i-1,j)+dfs(i,j-1);
return dfs(i-1,j)+dfs(i,j-1);
}
};
动态内存代码:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m+1,vector<int>(n+1));
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];
}
};
三:最长递增子序列
题目:
给出一个整数数组nums,找到其中最长的严格递增子序列的长度,子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序,例如[3,6,2,7]是数组[0,3,1,6,2,2,7]的一个子序列
方法:
首先利用递归思想进行求解:
先把每一个位置的最长子序列的长度得到,在遍历过程中用一个变量接受最大的那个值
先在函数中用循环将每一个位置都传进dfs函数,然后在dfs函数中,从当前位置往后遍历,得到其的最长序列的长度
int dfs(int pos,vector<int>& nums)(递归函数的创立)
要用记忆化搜索将每个位置的长度记录下来,并要传进dfs函数中即可:
int dfs(int pos,vector<int>& nums,vector<int>& memo)
记忆化搜索代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> memo(n);
int deep = 0;
//找到每一个数为开始时的最长递增序列,并返回其长度
for(int i = 0;i<n;i++)
{
deep = max(deep,dfs(i,nums,memo)+1);
}
return deep;
}
int dfs(int pos,vector<int>&nums,vector<int>& memo)
{
if(memo[pos]!=0) return memo[pos];
int deep = 0;
for(int i = pos+1;i<nums.size();i++)
{
if(nums[pos]<nums[i])
deep = max(deep,dfs(i,nums,memo)+1);
}
memo[pos] = deep;
return deep;
}
};
动态内存管理代码:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n,1);
int deep = 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);
}
}
deep = max(deep,dp[i]);
}
return deep;
四:猜数字大小2
题目:
我们正在玩一个猜数字游戏,游戏规则如下:
(1)我从1到n之间选择一个数字
(2)你来猜我选了哪个数字
(3)如果你猜到正确的数字,就会赢得游戏
(4)如果你猜错了,那么我会告诉你,我选的数字比你的更大或者更小,并且你需要继续猜
(5)每当你猜了数字x并且猜错了的时候,你需要支付金额为x的现金。如果你花光了钱,就会输掉游戏
给你一个特定的数字n,返回能够确保你获胜的最小现金数,不管我选择哪个数字
方法:
从一个区间里面进行左右两个子树的现金数,得到它们之中最大的数,然后与下一个区间里面的获得的最大值,之后将每个区间的值进行比较,得到它们之间的最小值
int dfs(int left, int right);
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 head = left;head<=right;head++)//头结点
{
int x = dfs(left,head-1);
int y = dfs(head+1,right);
ret = min(ret,head+max(x,y));
}
memo[left][right] = ret;
return ret;
}
};