【算法/学习】:记忆化搜索

news2025/1/10 6:17:26

✨                                                 落魄谷中寒风吹,春秋蝉鸣少年归        🌏 

📃个人主页:island1314

 🔥个人专栏:算法学习

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


🚀引言:

基本概念

记忆化搜索(Memoization Search):是一种通过存储已经遍历过的状态信息,从而避免对同一状态重复遍历的搜索算法。

主要特点和应用场景包括:

  1. 避免重复计算: 记忆化搜索算法通过缓存已经计算过的结果,以避免对相同输入进行重复计算。这在递归算法中特别有用,因为递归往往会导致相同的子问题被反复解决。
  2. 提高效率: 通过保存中间计算结果,记忆化搜索算法能够大幅减少算法的时间复杂度,从指数级别降低到多项式级别。

动态规划: 记忆化搜索在动态规划中经常被使用。动态规划是一种解决优化问题的方法,通常包含递归和子问题重叠的特点。记忆化搜索能够避免重复计算,使得动态规划算法更加高效。
递归算法优化: 记忆化搜索主要用于优化递归算法。在递归调用中,如果存在相同的输入参数,记忆化搜索算法将直接返回已经计算过的结果,而不是重新执行计算。
应用于搜索问题: 记忆化搜索不仅用于动态规划,还可以应用于搜索问题,特别是深度优先搜索中的状态记忆。

经典的例子包括斐波那契数列的递归实现、图的最短路径问题中的递归搜索等。在实现记忆化搜索时,通常需要使用数据结构(如哈希表、数组等)来保存已经计算过的结果。

记忆化搜索和动态规划都是一种用于优化递归算法的技术,它们有很多相似之处,但也存在一些关键的区别

 记忆化搜索与递归相同与不同

相同之处

  •  重叠子问题: 记忆化搜索和动态规划都针对具有重叠子问题性质的问题。这意味着问题可以被划分为许多相似的子问题,这些子问题在解决整体问题时会被多次重复计算。
  •  优化递归: 两者都旨在优化递归算法。递归算法通常会导致相同的子问题被反复解决,而记忆化搜索和动态规划都致力于避免这种重复计算。
  •  缓存中间结果: 记忆化搜索和动态规划都使用某种形式的缓存来存储已经计算过的中间结果,以便在需要时直接返回结果,而不是重新计算。

不同之处

  1. 自顶向下 vs 自底向上: 记忆化搜索是自顶向下的方法,从大问题开始,逐步分解为子问题,并缓存这些子问题的结果。动态规划是自底向上的方法,从最小的子问题开始解决,逐步构建出整个问题的解。
  2. 递归 vs 迭代: 记忆化搜索通常使用递归实现,通过递归调用来解决问题。动态规划则使用迭代的方式,通过循环来计算并填充表格或数组。
  3. 状态转移方程 vs 递归调用: 在动态规划中,通常通过状态转移方程来描述问题的子问题之间的关系,从而构建解。而在记忆化搜索中,通常直接使用递归调用来表示问题的分解。
  4. 使用场景: 记忆化搜索通常更适用于处理问题的子问题规模较小,问题的状态转移方程比较复杂,递推关系不是很明确,问题适合转换为递归形式,并且递归深度不会太深。而动态规划则更适用于处理问题的子问题规模较大的情况,问题的状态转移方程比较简单,递归关系比较明确。问题不太适合转换为递归形式,或者递归深度过大容易导致栈溢出

总的来说,当递归中出现了大量完全相同的问题时,就会用到记忆化搜索和动态规划去优化递归算法的技术,但它们的实现方式和问题解决的思路有一些不同。在解决问题时,根据具体的情况选择使用记忆化搜索或动态规划能够更好地满足问题的需求。

记忆化搜索的解题步骤

我们在使用记忆化搜索解决问题的时候,其基本步骤如下:

  1. 写出问题的动态规划「状态」和「状态转移方程」。

  2. 定义一个缓存(数组或哈希表),用于保存子问题的解。

  3. 定义一个递归函数,用于解决问题。在递归函数中,首先检查缓存中是否已经存在需要计算的结果,如果存在则直接返回结果,否则进行计算,并将结果存储到缓存中,再返回结果。

  4. 在主函数中,调用递归函数并返回结果。

让我们来看一些相关题目,加深对于记忆化搜索的理解吧

1. 斐波那契数

图解:

下面我们写出两种写法

递归写法:

int dfs(int n)
{
	if (n == 0 || n == 1) return n;
	return dfs(n - 1) + dfs(n - 2);
}

int fib(int n) {
	return dfs(n);
}

记忆化搜索写法:

int memo[31];
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);
}


int fib(int n) {
	//初始化为 -1
	memset(memo, -1, sizeof memo);
	return dfs(n);
}

动态规划写法:

int dp[31];
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];
}

2. 不同路径

思路:

       同样这里如果使用普通的递归深度遍历,会有大量重复计算导致时间复杂度过高,所以这里要使用记忆化搜索来辅助递归算法,达到线性的时间复杂度,我们计算每个格子的路径,相当于上面格子的路径加上左边格子的路径一直递推,完成递归的逆推,同样动态规划也是按照这个逻辑顺推过去

图解如下:

递归写法:(超时)

int dfs(int i, int j)
{
	if (i == 0 || j == 0) return 0;
	if (i == 1 && j == 1) return 1; // 起始位置(1,1)为1

	return dfs(i - 1, j) + dfs(i, j - 1); // 只能往右下走
}

int uniquePaths(int m, int n) {
	return dfs(m, n);
}

记忆化搜索:

int dfs(int i, int j, vector<vector<int>>& memo)
{
	if (memo[i][j] != 0) return memo[i][j];
	
	if (i == 0 || j == 0) return 0;
	if (i == 1 && j == 1) {
		memo[i][j] = 1;
		return 1;
	}

	memo[i][j] = dfs(i - 1, j, memo) + dfs(i, j - 1, memo);
	return memo[i][j];
}

int uniquePaths(int m, int n) {
	vector<vector<int>> memo(m + 1, vector<int>(n + 1));
	return dfs(m, n, memo);
}

动态规划:

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];
}

3. 最长递增子序列

思路:

  暴力枚举每个起点,然后在每个起点的基础上往后枚举元素,然后记录最大长度即可

1、递归函数头 :int (dfs pos) 

  • 返回以pos为起点最长递增子序列长度

2、递归函数函数体

  • for(i = pos + 1 ~ n ) 找到 max( dfs(i)  + 1 ) 即可

3、递归出口:该题不用递归出口,因为基本不会越界

递归写法:(超时)

int dfs(int pos, vector<int>& nums)
{
	int ret = 1; // ret 初始化为 1,为一个字符时就是1
	for (int i = pos + 1; i < nums.size(); i++) {
		if (nums[i] > nums[pos]) { // 找到以 i 为起点的最大
			ret = max(ret, dfs(i, nums) + 1);
		}
	}
	return ret;
}

int lengthOfLIS(vector<int>& nums) {
	int ret = 0;
	for (int i = 0; i < nums.size(); i++) // 以任意位置为起点的最大值
		ret = max(ret, dfs(i, nums)); 
	return ret;
}

记忆化搜索:

int dfs(int pos, vector<int>& nums, vector<int>& memo)
{
	if (memo[pos] != 0) return memo[pos]; //剪枝

	int ret = 1; // ret 初始化为 1,为一个字符时就是1
	for (int i = pos + 1; i < nums.size(); i++) {
		if (nums[i] > nums[pos]) { // 找到以 i 为起点的最大
			ret = max(ret, dfs(i, nums , memo) + 1);
		}
	}
	memo[pos] = ret;
	return ret;
}

int lengthOfLIS(vector<int>& nums) {
	int ret = 0, n = nums.size();
	vector<int> memo(n);
	for (int i = 0; i < n; i++) // 以任意位置为起点的最大值
		ret = max(ret, dfs(i, nums, memo)); 
	return ret;
}

动态规划:

int lengthOfLIS(vector<int>& nums) {
	int n = nums.size(), ret = 0; //记录dp表内的最大值
	//表示以某个位置为起点的最大值
	vector<int> dp(n, 1); //初始为1
	// 填表顺序:从后往前

	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;
}

4. 猜数字大小II

思路:

这里我们最简单的办法就是使用dfs暴力搜索每个猜数字的情况,在左右子树中找到较小的数累加,完成比对,加上记忆化搜索即可。

递归写法:(超时)

int dfs(int l, int r)
{
	if (l >= r) return 0;
	int ret = INT_MAX;
	for (int head = l; head <= r; head++) //选择头节点
	{
		//获取当前以head为头节点左右子树的最大值,之所以获取最大值,是因为要确保自己能够获胜
		int x = dfs(l, head - 1);
		int y = dfs(head + 1, r);
		ret = min(ret, head + max(x, y)); // 获胜的最小值
	}
	return ret;
}

int getMoneyAmount(int n) {
	return dfs(1, n);
}

记忆化搜索:

int memo[205][205];
int dfs(int l, int r){
	if (l >= r) return 0;
	if (memo[l][r] != 0) return memo[l][r]; //表示已经遍历过了
	
	int ret = INT_MAX;
	for (int head = l; head <= r; head++) //选择头节点
	{
		//获取当前以head为头节点左右子树的最大值,之所以获取最大值,是因为要确保自己能够获胜
		int x = dfs(l, head - 1);
		int y = dfs(head + 1, r);
		ret = min(ret, head + max(x, y)); // 获胜的最小值
	}
	memo[l][r] = ret;
	return ret;
}

int getMoneyAmount(int n) {
	return dfs(1, n);
}

5. 矩阵中的最长递增路径

思路:

        Dfs:从一个单元格开始进行深度优先搜索,即可找到从该单元格开始的最长递增路径。对每个单元格分别进行深度优先搜索之后,即可得到矩阵中的最长递增路径的长度。

具体可参考 【算法/学习】:flood算法-CSDN博客

但是这题如果仅仅使用 朴素深度优先搜索就会超时,因为进行了大量的重复计算,同一个单元格会被访问多次,每次访问都要重新计算。由于同一个单元格对应的最长递增路径的长度是固定不变的,故可用记忆化的方法进行优化。用矩阵 memo 作为缓存矩阵,已经计算过的单元格的结果存储到缓存矩阵中。

        记忆化深度优先搜索:当访问到一个单元格 (i,j) 时,如果 memo[i][j]=0,说明该单元格的结果已经计算过,则直接从缓存中读取结果,如果 memo[i][j]=0,说明该单元格的结果尚未被计算过,则进行搜索,并将计算得到的结果存入缓存中。

递归写法:(超时)

int n, m;
int dir[4][2] = {
	{-1,0},{1,0},{0,-1},{0,1}
};

int dfs(vector<vector<int>>& matrix, int i, int j) {
	int ret = 1;
	for (int k = 0; k < 4; k++) {
		int x = i + dir[k][0], y = j + dir[k][1];
		if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
			ret = max(ret, dfs(matrix, x, y) + 1);
		}
	}
	return ret;
}


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 memo[205][205];
int n, m;
int dir[4][2] = {
	{-1,0},{1,0},{0,-1},{0,1}
};

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 + dir[k][0], y = j + dir[k][1];
		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;
}


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;
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2040671.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构——队列的讲解(超详细)

前言&#xff1a; 我们在之前刚讲述完对于栈的讲解&#xff0c;下面我们在讲另一个类似栈的数据结构——队列&#xff0c;它们都是线性表&#xff0c;但结构是大有不同&#xff0c;下面我们直接进入讲解&#xff01; 目录 1.队列的概念和结构 1.1.队列的概念 1.2.队列的结构 2.…

基于Python的去哪儿网数据采集与分析可视化大屏设计与实现

摘要 本文旨在介绍如何利用Python进行去哪儿网景点数据的采集与分析。通过采集去哪儿网上的景点数据&#xff0c;我们可以获取大量的旅游相关信息&#xff0c;并基于这些数据进行深入分析和洞察&#xff0c;为旅游行业、市场营销策略以及用户个性化推荐等提供支持。 本文将使用…

MySQL(DQL)

一&#xff0c;SQL语言分类 &#xff08;1&#xff09;数据查询语言&#xff08;DQL&#xff1a;Data Query Language&#xff09;其语句&#xff0c;也称为 “数据检索语句”&#xff0c;用以从表中获得数据&#xff0c;确定数据怎样在应用程 序给出。关键字 SELECT 是 DQL&a…

Python打包命令汇总

1、pyinstaller打包 环境安装&#xff1a;pip install pyinstaller 网络不好可以通过 -i 指定安装源&#xff1a;pip install pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple/安装完成后通过&#xff1a;pyinstaller --version 查看是否安装成功 打包单个脚本&…

操作系统笔记二

虚拟内存 把不常用的数据放到硬盘上去&#xff0c;常用的代码或者数据才加载到内存&#xff0c;来实现虚拟的大内存的感觉 覆盖技术 目标&#xff1a;在较小内存运行较大程序。 原理&#xff1a;把程序按自身逻辑结构划分若干功能上相对独立的程序模块。不回同时执行的模块共…

FreeRTOS学习笔记(一)—— 裸机和RTOS,Freertos移植(MDK),stm32cubeIDE使用Freertos

FreeRTOS学习笔记&#xff08;一&#xff09;—— 裸机和RTOS&#xff0c;Freertos移植&#xff08;MDK&#xff09;&#xff0c;stm32cubeIDE使用Freertos 文章目录 FreeRTOS学习笔记&#xff08;一&#xff09;—— 裸机和RTOS&#xff0c;Freertos移植&#xff08;MDK&#…

uniapp/vue个性化单选、复选组件

个性化单选和复选组件在网页设计中非常常见&#xff0c;它们不仅能够提升用户界面的美观度&#xff0c;还能改善用户体验。此组件是使用vue uniapp实现的个性化单选复选组件。设计完成后&#xff0c;点击生成源码即可。 拖动组件过设计区 每行显示数量 默认支持每行三个&#…

Maven-学习首篇

目录 Maven简介基本概念&特点Maven的安装与配置Maven基础概念及使用方法Maven的项目结构Maven的使用Maven的依赖管理Maven的生命周期和插件常见疑问Maven的插件机制是如何工作的&#xff1f;Maven的POM文件主要包含哪些内容&#xff1f;Maven的生命周期包括哪些阶段&#x…

【C++语言】list的构造函数与迭代器

1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xff0c;并且该容器可以前后双向迭代。 2. list的底层是双向链表结构&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点…

C++中的IO流

目录 1.C语言的输入与输出 2.流是什么 3.CIO流 标准IO流 IO流的四个标志 C文件IO流 4.stringstream的简单介绍 1.C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键 盘)读取数据&#xff0c;并将值存放在变…

钢铁百科:A572Gr60和SA572Gr60材质分析、A572Gr60和SA572Gr60简介

A572Gr60和SA572Gr60是两种常用的结构钢板&#xff0c;它们在材质、执行标准、化学成分、力学性能、交货状态、应用范围和常用规格方面有所不同。 材质&#xff1a; A572Gr60&#xff1a;属于美国材料与试验协会&#xff08;ASTM&#xff09;标准下的A572系列高性能结构钢&…

UIAbility组件基础(一)

一、概述 UIAbility组件是一种包含UI的应用组件&#xff0c;主要用于和用户交互。UIAbility组件是系统调度的基本单元&#xff0c;为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。每一个UIAbility组件实例都会在最近任务列表中显示一个对应的任务。 U…

自研低代码海报制作平台学习分享计划

vue3组件库开发前面咱卷完了JuanTree组件&#xff0c;接下来一起来卷vue3低代码海报制作平台的基础组件实现。首先是拖拽基础组件的开发&#xff0c;整好把前面学习的知识点再运用进来。 文章目录 效果演示基本拖拽区域拖拽旋转其他效果待实现 录屏说明 看一步步实现的效果&…

C++--类和对象(二)

类和对象的基础定义可参看&#xff1a;C--类和对象&#xff08;一&#xff09;-CSDN博客 本篇讲述类和对象里相当重要的几个成员函数 目录 类的默认成员函数&#xff1a; 1.构造函数 2.析构函数 3.拷贝构造函数 &#xff08;1&#xff09;无限递归调用拷贝构造 &#xff…

在Ubuntu中重装Vscode(没有Edit Configurations(JSON)以及有错误但不标红波浪线怎么办?)

在学习时需要将vscode删除重装&#xff0c;市面上很多方法都不能删干净&#xff0c;删除之后拓展都还在。因此下面的方法可以彻底删除。注意&#xff0c;我安装时使用的是snap方法。 如果你的VScode没有Edit Configurations(JSON)&#xff0c;以及有错误但不标红波浪线的话&…

基于QT实现的TCP连接的网络通信(客户端)

上篇介绍了QT实现网络通信的服务器端&#xff0c;还没看服务器的朋友们先去上篇了解&#xff0c;这篇我来实现一下客户端的实现。 首先还是新建一个项目 选择mainwindow类 在通信前将.pro文件的第一行代码中追加network 窗口搭建 在mainwindow.ui中完成一下窗口的搭建 首先在…

序列建模之循环和递归网络 - 双向RNN篇

序言 在序列建模的广阔领域中&#xff0c;循环神经网络&#xff08; RNN \text{RNN} RNN&#xff09;以其独特的循环结构&#xff0c;在处理序列数据方面展现出了强大的能力。然而&#xff0c;传统的单向 RNN \text{RNN} RNN在处理某些复杂任务时&#xff0c;如自然语言处理中…

基于51单片机的士出租车计价器proteus仿真

地址&#xff1a;https://pan.baidu.com/s/1-GOrUrYlyGZFfkiiO6i5yg 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectron…

「Unity3D」TextMeshPro-Text(UI)无法拖放到TextMeshPro的属性面板上

继承MonoBehaviour&#xff0c;然后定义public TextMeshPro textPro&#xff0c;属性面板上就会有TextMeshPro的拖放槽&#xff08;slot&#xff09;&#xff0c;以配置含有TextMeshPro的组件对象&#xff08;GameObject&#xff09;。 但此时会发现&#xff0c;含有TextMeshPr…

李沐老师动手深度学习pytorch版本的读取fashion_mnist数据并用AlexNet模型训练,其中修改为利用本地的数据集训练

李沐老师的d2l.load_data_fashion_mnist里面没有root参数&#xff0c;所以只会下载&#xff0c;不能利用本地的fashion_mnist数据。所以我使用torchvision 的datasets里面FashionMNIST方法&#xff0c;又由于李沐老师此处是利用AlexNet模型来训练fashion_mnist数据&#xff0c;…