所谓记忆化,就是在搜索的过程中将某个已经搜索到的值记录在一个记忆化数组中,下次再遇到时则直接返回记忆化数组中的值
例题1:
先来道水题,求斐波那契数列第n项的值。
样例输入:
5
样例输出:
5
大水题啊兄弟们!
直接写出递归代码:
int get(int n) {
if(n == 1 || n == 2) return 1;
else return get(n - 1) + get(n - 2);
}
但是这样时间会有很大的浪费
比如说求get(5)时,需要求get(4)和get(3),求get(4)时,又要再求一遍get(3),十分的麻烦
这个时候,记忆化就派上用场了
定义一个dp数组,当我们求出get(n-1)的值时,直接将get(n-1)的值赋给dp[n-1],之后再求get(n-1)时,就可以直接返回dp[n-1]了
带有记忆化的代码:
int dp[1005];
int get(int n) {
if(n == 1 || n == 2) return 1;
if(dp[n] != 0) return dp[n]; //如果dp[n]里面有值,则直接返回dp[n]的值
dp[n - 1] = get(n - 1);
dp[n - 2] = get(n - 2);
return dp[n - 1] + dp[n - 2];
}
好,做完一道水题(rp++)后,来看看两道典型例题
No.1:
剪纸是我国一项民间艺术,这不,编程社的同学也想体验体验
有一张a * b的蜡光纸,每次只能沿着行边或者列边剪一刀,要么横着剪断,要么竖着剪断。每次剪一刀要付出一定的代价,代价是:剪断这条边长的平方!(举个栗子:有一张2 * 3的矩形蜡光纸,你可以横向剪,可以得到2张1 * 3的小蜡光纸,代价为3^2=9;你也可以纵向剪,就可以得到1张2 * 1和1张2 * 2的小蜡光纸,花费为2^2=4)。
现在限定一个p,(p ≤ min(n*m, 50)), 问最后得到面积为p个单位的蜡光纸时的最小代价?
样例输入:
4
2 2 1
2 2 3
2 2 2
2 2 4
样例输出:
5
5
4
0
拿到这道题,不知道各位大佬们能不能迅速地打出爆搜代码,如果能,恭喜你,加一个记忆化之后就AC了(rp++)。如果你不能(请反思一下你自己),那你就往下看吧
首先,我们知道,a和b的顺序不影响最后的结果,也就是说:2 3 2 和 3 2 2最后求出来的值时一样的,那我们可以统一一下a和b的顺序,小的在前,大的在后,方便我们dfs里面的处理
代码:
int a, b, p;
SF("%d%d%d", &a, &b, &p);
if(a > b) swap(a, b);//小的在前,大的在后
好,现在我们就可以开始打我们的爆搜了
首先,我们可以沿着b这条边来剪,也就是将a的长度减小,将这张纸分为两片,代价加上b * b
反之则,我们就沿着a这条边来剪,也就是将b的长度减小,将这张纸分为两片,代价加上a * a
再添一句题目中没有说到的条件哈:不一定要沿着中线来剪,随便在哪里减都是可以的。比如一张5 * 6的纸,沿着5这条边来减,可以将纸剪为1 * 5和5 * 5两个部分,不一定非要剪成3 * 5和3 * 5
好,上爆搜代码(注意看注释啊!):
int dfs(int a, int b, int p) { //返回的东西为将a * b的纸剪为面积为p最少的代价
if(a * b == p || !p) return 0; //特判两种情况,一种情况不用剪,另一种情况剪完了
int ans = 0x3f3f3f3f; //ans初始化成一个极大值
for(int i = 1; i <= a / 2; i++) { //沿着b剪,暴力搜索一张纸的剩下的a可能的值
for(int j = 0; j <= p; j++) { //枚举需要剪成的面积
ans = min(ans, dfs(i, b, j) + dfs(a - i, b, p - j) + b * b);
//dfs(i, b, j)表示将i * b的纸剪成面积为j的纸所需要的代价
//dfs(a - i, b, p - j)表示将剩下的纸剪成面积为p - j的纸所需要的代价
//b * b表示需要加上的代价
}
}
for(int i = 1; i <= b / 2; i++) { //同上,只不过是沿着a剪的
for(int j = 0; j <= p; j++) {
ans = min(ans, dfs(a, i, j) + dfs(a, b - i, p - j) + a * a);
}
}
return ans; //返回答案
}
各位大佬们可以好好瞅一瞅
打到这里,这道题就完成了95%了,只差记忆化
我们可以用一个三维数组dp[a][b][p]记录将一张a * b剪成面积为p所需要的最少的代价,当重复求的时候直接返回dp[a][b][p]就可以了
记忆化搜索代码:
int dfs(int a, int b, int p) {
if(a * b == p || !p) return 0;
if(dp[a][b][p]) return dp[a][b][p]; //重复求时直接返回
int ans = 0x3f3f3f3f;
for(int i = 1; i <= a / 2; i++) {
for(int j = 0; j <= p; j++) {
ans = min(ans, dfs(i, b, j) + dfs(a - i, b, p - j) + b * b);
}
}
for(int i = 1; i <= b / 2; i++) {
for(int j = 0; j <= p; j++) {
ans = min(ans, dfs(a, i, j) + dfs(a, b - i, p - j) + a * a);
}
}
return dp[a][b][p] = ans; //记录将一张a * b剪成面积为p所需要的最少的代价
}
这就是记忆化的魅力,虽然只多了两行,但时间却快了无数
No.2:
有4根竖直的管子,每个管子里有n颗糖果叠成一堆,有一个篮子,最多可以放5颗糖果。每次可以取走任意一个管子的最上方的一颗糖果放到篮子里,若篮子里有两颗糖的颜色相同,可以将这一对糖果放进口袋。求最多可以放多少对糖果到口袋里。
样例输入:
5
1 2 3 4
1 5 6 7
2 3 3 3
4 9 8 6
8 7 2 1
1
1 2 3 4
3
1 2 3 4
5 6 7 8
1 2 3 4
0
样例输出:
8
0
3
这道题也是一道记忆化搜索的好题
我们用a, b, c, d记录4个管子中正在拿第几个数字。比如a = 1, b = 1, c = 1, d = 1时,就说明4个管子中正在拿第一个数字,我们就可以拿mp[1][1]或mp[1][2]或mp[1][3]或mp[1][4],即mp[a][1]或 mp[b][2]或mp[c][3]或mp[d][4]
再回过头来看这道题,就能够轻而易举地打出爆搜
代码(有亿点点长,但自我感觉挺好理解的):
bool f[45];
void dfs(int a, int b, int c, int d, int cnt, int sum) {
//a, b, c, d记录4个管子中正在拿第几个数字,cnt为篮子里现在有的数字,sum为当前答案
if(cnt == 5) {
ans = max(ans, sum); //记录答案
return;
}
if(a <= n) { //如果还可以在a管子里面取数
if(f[mp[a][1]]) { //如果篮子里有现在要取的数
f[mp[a][1]] = 0; //就把篮子里数的取出来
dfs(a + 1, b, c, d, cnt - 1, sum + 1);
//搜索下一个状态,因为把篮子里的数取了出来,所以cnt - 1,答案的对数sum + 1
f[mp[a][1]] = 1; //回溯
}
else { //如果篮子里没有现在要取的数
f[mp[a][1]] = 1; //将该数放进篮子中
dfs(a + 1, b, c, d, cnt + 1, sum);
//搜索下一个状态,因为放了一个数进入篮子中,所以cnt + 1,答案不变
f[mp[a][1]] = 0; //回溯
}
}
if(b <= n) { //同上,搜索b管子
if(f[mp[b][2]]) {
f[mp[b][2]] = 0;
dfs(a, b + 1, c, d, cnt - 1, sum + 1);
f[mp[b][2]] = 1;
}
else {
f[mp[b][2]] = 1;
dfs(a, b + 1, c, d, cnt + 1, sum);
f[mp[b][2]] = 0;
}
}
if(c <= n) { //同上,搜索c管子
if(f[mp[c][3]]) {
f[mp[c][3]] = 0;
dfs(a, b, c + 1, d, cnt - 1, sum + 1);
f[mp[c][3]] = 1;
}
else {
f[mp[c][3]] = 1;
dfs(a, b, c + 1, d, cnt + 1, sum);
f[mp[c][3]] = 0;
}
}
if(d <= n) { //同上,搜索d管子
if(f[mp[d][4]]) {
f[mp[d][4]] = 0;
dfs(a, b, c, d + 1, cnt - 1, sum + 1);
f[mp[d][4]] = 1;
}
else {
f[mp[d][4]] = 1;
dfs(a, b, c, d + 1, cnt + 1, sum);
f[mp[d][4]] = 0;
}
}
ans = max(ans, sum); //最后再看看是否能够更新答案
}
但是这样写慢的要死,第一个样例要跑很久(虽然能跑出来正确答案,可以拿一点分)
这个时候记忆化搜索就显得非常有必要了
和上一题的思路一样,我们可以定义一个四维数组dp,每一次dfs后将答案变成一个返回值,赋值到dp中。
来来来,看着代码说才有意思:
bool f[45];
int dp[45][45][45][45];
int dfs(int a, int b, int c, int d, int cnt, int sum) { //记忆化搜索是有返回值滴~
if(cnt == 5) {
ans = max(ans, sum);
return dp[a][b][c][d] = sum; //将当前求出的答案记录在dp数组中
}
if(dp[a][b][c][d]) return dp[a][b][c][d]; //如果已经求过当前状态的值,则直接返回
if(a <= n) {
if(f[mp[a][1]]) {
f[mp[a][1]] = 0;
dp[a + 1][b][c][d] = dfs(a + 1, b, c, d, cnt - 1, sum + 1);
//将当前函数的返回值记录下来,下面对dp的赋值道理都一样,不再赘述
f[mp[a][1]] = 1;
}
else {
f[mp[a][1]] = 1;
dp[a + 1][b][c][d] = dfs(a + 1, b, c, d, cnt + 1, sum);
f[mp[a][1]] = 0;
}
}
if(b <= n) {
if(f[mp[b][2]]) {
f[mp[b][2]] = 0;
dp[a][b + 1][c][d] = dfs(a, b + 1, c, d, cnt - 1, sum + 1);
f[mp[b][2]] = 1;
}
else {
f[mp[b][2]] = 1;
dp[a][b + 1][c][d] = dfs(a, b + 1, c, d, cnt + 1, sum);
f[mp[b][2]] = 0;
}
}
if(c <= n) {
if(f[mp[c][3]]) {
f[mp[c][3]] = 0;
dp[a][b][c + 1][d] = dfs(a, b, c + 1, d, cnt - 1, sum + 1);
f[mp[c][3]] = 1;
}
else {
f[mp[c][3]] = 1;
dp[a][b][c + 1][d] = dfs(a, b, c + 1, d, cnt + 1, sum);
f[mp[c][3]] = 0;
}
}
if(d <= n) {
if(f[mp[d][4]]) {
f[mp[d][4]] = 0;
dp[a][b][c][d + 1] = dfs(a, b, c, d + 1, cnt - 1, sum + 1);
f[mp[d][4]] = 1;
}
else {
f[mp[d][4]] = 1;
dp[a][b][c][d + 1] = dfs(a, b, c, d + 1, cnt + 1, sum);
f[mp[d][4]] = 0;
}
}
ans = max(ans, sum);
return dp[a][b][c][d] = sum; //最后还要再记忆化一下(亲测,不写会RE)
}
好了,两道题目都讲完了,相信各位大佬已经对记忆化搜索有了很大的认知,祝各位大佬在以后的学习生活中风生水起,AKIOI!
确定不来水水?rp += 0x3f3f3f3f