【学习笔记】记忆化搜索
记忆化搜索
建议搭配食用。
前置知识:
- 深度优先搜索 DFS
概念:
搜索通常通过递归来实现,但是递归过程中往往有很多结果被重复计算,因此降低了搜索的效率。
因此记忆化搜索就是再递归的过程中记录已经遍历过的状态与结果,用到的时候再直接取出,避免了重复运算。
实现:
oiwiki 讲的很好,但是我要再赘述一遍:
[NOIP2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 个整数 ()和 (),用一个空格隔开, 代表总共能够用来采药的时间, 代表山洞里的草药的数目。
接下来的 行每行包括两个在 到 之间(包括 和 )的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
样例 #1
样例输入 #1
70 3 71 100 69 1 1 2
样例输出 #1
3
提示
【数据范围】
- 对于 的数据,;
- 对于全部的数据,。
【题目来源】
NOIP 2005 普及组第三题。
这道题其实我记得我是用背包 dp 做的,但是今天我们先来进行一个朴素的 DFS 做法。
代码写了注释了,学过 dfs 都能懂
朴素搜索
#include<bits/stdc++.h> using namespace std; typedef long double llf; typedef long long intx; const int maxn=105; int n,t; //总共n株草药,总时间为t int cost[maxn],val[maxn]; //cost是草药的花费时间,val是权值 int ans; void input(){ scanf("%d %d",&t,&n); for(int i=1;i<=n;++i){ scanf("%d %d",&cost[i],&val[i]); } } void dfs(int pos,int lost,int sval){ //pos记录当前准备选第几个物品,lost表示剩下的时间,sval表示已获得的价值 if(lost<0) return; //不剩时间了没有继续的必要 if(pos==n+1){ //草药已经摘到最后一株,更新答案后返回 ans=max(ans,sval); return; } dfs(pos+1,lost,sval); dfs(pos+1,lost-cost[pos],sval+val[pos]); //选或者不选 } int main(){ input(); dfs(1,t,0); printf("%d\n",ans); return 0; }
然后 T 啦!!
同一状态会被访问多次,比如我们把采草药叫做状态 ,不采草药叫做 。
(利用了状态压缩的思想解释被访问多次的状态,但是这道题由于 范围过大不适合状压 dp 来做)
对于 ,状态 来说,之后还有六个状态是未知的,但是这个状态会被重复走很多次。
具体想知道运行时间有多大差距可以看木偶Roy的运行结果对比。
那么我们专门来为这个状态开一个数组 表示记录的搜索返回值 ,再次访问的时候直接返回数值即可,这样也使得 dfs 省了一个参数 。
而对于初始化来讲,我们可以使其值为一个绝对不可能的值表示没有搜索到过(本题为 )。
有代码实现:
记忆化搜索
#include<bits/stdc++.h> using namespace std; #define MYMIN -191981000 typedef long double llf; typedef long long intx; const int maxn=105,maxm=1005; int n,t; //总共n株草药,总时间为t int cost[maxn],val[maxn]; //cost是草药的花费时间,val是权值 int memo[maxn][maxm]; //记忆状态 int ans; void input(){ scanf("%d %d",&t,&n); for(int i=1;i<=n;++i){ scanf("%d %d",&cost[i],&val[i]); } } int dfs(int pos,int lost){ //pos记录当前准备选第几个物品,lost表示剩下的时间 if(memo[pos][lost]!=-1) return memo[pos][lost]; //如果已经访问过就直接返回 if(pos==n+1){ //草药已经摘到最后一株 return memo[pos][lost]=0; } int dfs1,dfs2=MYMIN; dfs1=dfs(pos+1,lost); //不采药 if(lost>=cost[pos]){ //如果还能继续采才有dfs2 dfs2=dfs(pos+1,lost-cost[pos])+val[pos]; } return memo[pos][lost]=max(dfs1,dfs2); //将当前状态存下来,如果没有dfs2对结果无影响,因为dfs2的初始值是最小值 } int main(){ memset(memo,-1,sizeof(memo)); input(); ans=dfs(1,t); printf("%d\n",ans); return 0; }
对于这道题我们总结一下记忆化搜索的特点:
-
不依靠外部变量 记录,往往意味着函数具有返回值。
-
因为答案是返回值,所以不成为函数的参数。
-
对于一组参数,其返回值相同。
记忆化搜索与递推
求动态规划的问题上,记忆化搜索与递推还蛮像的,毕竟记忆化搜索就是用递推递归实现的,oiwiki 给了一个代码比对,我就不写了:
递推
const int maxn = 1010; int n, t, w[105], v[105], f[105][1005]; int main() { cin >> n >> t; for (int i = 1; i <= n; i++) cin >> w[i] >> v[i]; for (int i = 1; i <= n; i++) for (int j = 0; j <= t; j++) { f[i][j] = f[i - 1][j]; if (j >= w[i]) f[i][j] = max(f[i][j], f[i - 1][j - w[i]] + v[i]); // 状态转移方程 } cout << f[n][t]; return 0; }
递推和记忆化搜索都保证了同一状态只被求解一次,因此复杂度相近。
但是他们的实现方式确实不同的:
-
递推通过明确的访问顺序来避免重复访问(如代码中的 与 都是有顺序的访问)。
-
记忆化搜索通过打标记的方式避免重复访问。(如 表示没有访问过)
两者各有优劣,很多递推可以用滚动数组优化,而且没有递推过程。
(滚动数组:动态规划中当前状态只与上一状态有关时,用 表示当前状态,^ 表示上一状态,可以将数组某一维的空间降为 ).
而记忆化搜索更好实现,有时在树上问题极为常见,而且还作为 数位 dp 的前置知识存在,与众多动态规划相连。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现