【学习笔记】记忆化搜索

记忆化搜索

oiwiki:记忆化搜索

建议搭配食用。

前置知识:

  • 深度优先搜索 DFS

概念:

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

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

实现:

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

[NOIP2005 普及组] 采药

题目链接

题目描述

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

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

输入格式

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

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

输出格式

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

样例 #1

样例输入 #1

70 3
71 100
69 1
1 2

样例输出 #1

3

提示

【数据范围】

  • 对于 \(30\%\) 的数据,\(M \le 10\)
  • 对于全部的数据,\(M \le 100\)

【题目来源】

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的运行结果对比

那么我们专门来为这个状态开一个数组 \(memo_{pos,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;
}

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

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

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

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

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

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

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

posted @ 2023-08-01 16:34  Sonnety  阅读(76)  评论(1编辑  收藏  举报