那么我们就永远不能停下脚步。|

Sonnety

园龄:2年粉丝:80关注:96

【学习笔记】记忆化搜索

记忆化搜索

oiwiki:记忆化搜索

建议搭配食用。

前置知识:

  • 深度优先搜索 DFS

概念:

搜索通常通过递归来实现,但是递归过程中往往有很多结果被重复计算,因此降低了搜索的效率。

因此记忆化搜索就是再递归的过程中记录已经遍历过的状态与结果,用到的时候再直接取出,避免了重复运算。

实现:

oiwiki 讲的很好,但是我要再赘述一遍:

[NOIP2005 普及组] 采药

题目链接

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 个整数 T1T1000)和 M1M100),用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。

接下来的 M 行每行包括两个在 1100 之间(包括 1100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

样例 #1

样例输入 #1

70 3
71 100
69 1
1 2

样例输出 #1

3

提示

【数据范围】

  • 对于 30% 的数据,M10
  • 对于全部的数据,M100

【题目来源】

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 啦!!

同一状态会被访问多次,比如我们把采草药叫做状态 1,不采草药叫做 0

(利用了状态压缩的思想解释被访问多次的状态,但是这道题由于 n 范围过大不适合状压 dp 来做)

对于 n==12,状态 1 0 0 1 1 1 来说,之后还有六个状态是未知的,但是这个状态会被重复走很多次。

具体想知道运行时间有多大差距可以看木偶Roy的运行结果对比

那么我们专门来为这个状态开一个数组 memopos,lost 表示记录的搜索返回值 sval,再次访问的时候直接返回数值即可,这样也使得 dfs 省了一个参数 sval

而对于初始化来讲,我们可以使其值为一个绝对不可能的值表示没有搜索到过(本题为 1)。

有代码实现:

记忆化搜索
#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;
}

对于这道题我们总结一下记忆化搜索的特点:

  • 不依靠外部变量 ans 记录,往往意味着函数具有返回值。

  • 因为答案是返回值,所以不成为函数的参数。

  • 对于一组参数,其返回值相同。

记忆化搜索与递推

求动态规划的问题上,记忆化搜索与递推还蛮像的,毕竟记忆化搜索就是用递推递归实现的,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;
}

递推和记忆化搜索都保证了同一状态只被求解一次,因此复杂度相近。

但是他们的实现方式确实不同的:

  • 递推通过明确的访问顺序来避免重复访问(如代码中的 ij 都是有顺序的访问)。

  • 记忆化搜索通过打标记的方式避免重复访问。(如 1 表示没有访问过)

两者各有优劣,很多递推可以用滚动数组优化,而且没有递推过程。

(滚动数组:动态规划中当前状态只与上一状态有关时,用 cur 表示当前状态,cur^1 表示上一状态,可以将数组某一维的空间降为 2).

而记忆化搜索更好实现,有时在树上问题极为常见,而且还作为 数位 dp 的前置知识存在,与众多动态规划相连。

posted @   Sonnety  阅读(102)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起