一、概念
记忆化搜索是通过记录已经遍历过的状态的信息,确保了每个状态只访问一次,避免对同一状态重复遍历的搜索实现方式。
二、例子–P1048 NOIP2005 普及组 采药
朴素的DFS做法
/*朴素的DFS做法:搜索时记录下当前枚举到第几个物品、所剩时间、已经获得的价值
同一个状态会被访问多次,时间复杂度是指数级别的,评测结果TLE*/
#include<bits/stdc++.h>
using namespace std;
int T,m,ans;
struct node{
int t,w;
}a[105];
void dfs(int p,int t1,int w1){
if(t1<0) return;
if(p==m+1){
ans=max(ans,w1);
return ;
}
dfs(p+1,t1,w1);
dfs(p+1,t1-a[p].t,w1+a[p].w);
}
int main(){
cin>>T>>m;
for(int i=1;i<=m;i++) cin>>a[i].t>>a[i].w;
dfs(1,T,0);
cout<<ans;
}
优化:记忆化搜索
用空间换取时间
/*记忆化搜索:数组mem记录每个dfs(p,t1)的返回值
用空间换取时间,时间复杂度为O( TM )*/
#include<bits/stdc++.h>
using namespace std;
int T,m;
struct node{
int t,w;
}a[105];
int mem[105][1005];
int dfs(int p,int t1){
if(mem[p][t1]!=(-1))return mem[p][t1];
if(p==m+1){
return mem[p][t1]=0;
}
int d1=-0x3f3f3f3f,d2; //注意初始化
if(t1>=a[p].t){
d1=dfs(p+1,t1-a[p].t)+a[p].w;
}
d2=dfs(p+1,t1);
return mem[p][t1]=max(d1,d2);
}
int main(){
memset(mem,-1,sizeof(mem));//注意初始化
cin>>T>>m;
for(int i=1;i<=m;i++) cin>>a[i].t>>a[i].w;
cout<<dfs(1,T);
}
递推(与记忆化搜索形式上高度相似)
两者使用相同的状态表示方式和类似的状态转移方程,递推的时间复杂度也为O( TM )
/*递推*/
#include<bits/stdc++.h>
using namespace std;
int T,m;
struct node{
int t,w;
}a[105];
int f[105][1005];//f[i][j] 时间j内 枚举到第i个药品获得的最大的价值
int main(){
cin>>T>>m;
for(int i=1;i<=m;i++) cin>>a[i].t>>a[i].w;
for(int i=1;i<=m;i++){
for(int j=0;j<=T;j++){
f[i][j]=f[i-1][j];
if(j>=a[i].t){
f[i][j]=max(f[i][j],f[i-1][j-a[i].t]+a[i].w);
}
}
}
cout<<f[m][T];
}
不同的是,递推通过设置明确的访问顺序来避免重复访问,记忆化搜索虽然没有明确规定访问顺序,但通过给已经访问过的状态打标记的方式,也达到了同样的目的。
记忆化搜索相较于递推比较方便地处理边界情况。但,记忆化搜索有时运行效率会低于递推。因此应该视题目选择更适合的实现方式。
三、写记忆化搜索的步骤
方法一
1.把这道题的 dp 状态和方程写出来
2.根据它们写出 dfs 函数
3.添加记忆化数组
方法二
·1.写出这道题的暴搜程序(最好是 dfs)
2.将这个 dfs 改成“无需外部变量”的 dfs
3.添加记忆化数组