题目链接:
蓝桥杯2014年第五届真题-地宫取宝 - C语言网 (dotcpp.com)
难度:
中等难度。
引用:
地宫取宝(DFS+动态规划:记忆化递归)-Dotcpp编程社区
蓝桥杯真题- 地宫取宝 动态规划 / 记忆化搜索 (C++)-Dotcpp编程社区
1 记忆式递归
记忆化搜索,本质还是 动态规划,只是实现方式采用了 深度优先搜索 的形式,但是它不像 深度优先搜索那样 重复 枚举所有情况,而是把已经计算的子问题保存下来,这样就和动态规划的思想不谋而合了。
地宫取宝(DFS+动态规划:记忆化递归)-Dotcpp编程社区
开辟一个缓存数组cache[x][y][max][cnt]将四种状态都记录
x,y:位置
max:当前手中宝物价值的最大值
cnt:当前手中宝物数
(x,y)想要到达p点,是向右还是向左,是拾起(x,y)格子处的宝物还是不拾起,无论是怎么到达p点的,如果在p点发现dfs(i,j,max,cnt)中i,j,max,cnt,和缓存中的一样,那就是重复搜索了。可以采用动态规划(dp):动归数组、逐步生成或者记忆性递归,这里采用记忆性递归,因此开辟一个缓存数组。搜索中保存子问题的值, 在搜索过程中若子问题的最优值已存在, 直接使用而避免重复搜索。
代码:
#include<iostream>
#include<algorithm>
#include<cstring>
#define MOD 1000000007
using namespace std;
int n, m, k;
int maze[50][50];
long long ans;
long long cache[50][50][14][13];//缓存数组,由题1< =n,m< =50, 0< =Ci< =12,1< =k< =12
long long dfs(int x, int y, int max, int cnt)//不再是void,而是返回long long 类型的方案数ans
{
//先查缓存,缓存里有,直接用
if (cache[x][y][max+1][cnt] != -1)//记忆化搜索
//此处为什么max+1,因为题目中宝物价值是:Ci (0< =Ci< =12),包含0,所以可能取到0,那我在main函数调用时初始化max只能是负数,否则如果起始点为0,我就不能拾起了,但数组下标又不能是负数,干脆直接化为max+1,max+1>=0说明拾起第一件宝物
return cache[x][y][max+1][cnt];
//如果越界或者当前手中宝物数大于k,退出
if (x == n || y == m || cnt > k)
return 0;
int cur = maze[x][y];//当前宝物价值
long long ans = 0;
//走到最后一格
if (x == n - 1 && y == m - 1)
{
//手中宝物数等于k 或者 手中宝物数比k少1,并且当前格子宝物价值大于手中宝物价值的最大值,可以捡起最后一个宝物,同样符合条件。因此方案数+1
if (cnt == k||(cnt==k-1&&cur>max))
{
ans++;
if (ans > MOD)//取模操作
ans = ans % MOD;
}
return ans;//返回方案数
}
//当前格子宝物价值大于手中宝物价值的最大值,并且拾起该宝物,将方案数叠加
if (cur > max)
{
ans += dfs(x, y + 1, cur, cnt + 1);//向右
ans += dfs(x + 1,y , cur, cnt + 1);//向下
}
//1. 当前当前格子宝物价值大于手中宝物价值的最大值,但不拾起
//2. 当前当前格子宝物价值小于手中宝物价值的最大值
ans += dfs(x, y + 1, max, cnt);//向右
ans += dfs(x + 1, y, max, cnt);//向下
//写入缓存
cache[x][y][max+1][cnt] = ans % MOD;
return cache[x][y][max+1][cnt];//返回方案数
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> maze[i][j];
memset(cache, -1, sizeof(cache));//初始化为-1
cout<<dfs(0, 0, -1, 0)<<endl;
return 0;
}
代码逻辑:
递归遍历所有位置,计算到每个位置之后继续走、可以符合条件结束的路径数量。
- 如果缓存中已经有:返回缓存中方案数
- 如果越界或者手中宝物数大于k: return 0
- 走到最后一格时,如果手中宝物数量等于k/k-1(说明该方案可行):ans++,return ans
- 当前宝物价值大于手上宝物最大价值,并拾起宝物:宝物数量+1,更新宝物最大价值,继续向右或向下走
- 当前宝物价值大于手上宝物最大价值,但不拾起宝物/当前宝物价值小于手上宝物最大价值,因此不拾起宝物:宝物数量不变,宝物最大价值不变,继续向右或向下走
2 动态规划
- 记忆化搜索是以使用备忘录避免重复计算来跳过重叠子问题的,而狭义动态规划是以设计巧妙的递推顺序来压根就不产生重叠子问题的。
- 多个状态下,动态规划通常会生成大量无效的状态,而记忆化搜索则不会,这是记忆化搜索在速度上有可能超越狭义动态规划的地方
蓝桥杯真题- 地宫取宝 动态规划 / 记忆化搜索 (C++)-Dotcpp编程社区
代码:
// 蓝桥杯真题 地宫取宝
// 动态规划法
#include <cstdio>
const int maxn = 52;
const int maxnC = 13;
const int Q = 1000000007; // 取模基数
int dp[maxn][maxn][maxnC][maxnC]; // 动态规划 dp[x][y][num][maxValue]
int C[maxn][maxn]; // 物品价值
int N, M, K;
int solve(){ // 迭代地动态规划
for (int i = 1; i <= N; i++){ // x 坐标
for (int j = 1; j <= M; j++){ // y 坐标
for (int k = 0; k <= K; k++){ // 背包内物品总数
for (int c = 0; c < maxnC; c++){ // 背包内单个物品的最大价值
if (i == 1 && j == 1){
if (k == 0 || (k == 1 && c > C[i][j]))
dp[i][j][k][c] = 1;
continue;
}
int t1 = 0, t2 = 0;
// 装当前物品
if (k > 0 && C[i][j] < c)
t1 = (dp[i][j-1][k-1][C[i][j]] + dp[i-1][j][k-1][C[i][j]]) % Q;
// 不装当前物品
t2 = (dp[i][j-1][k][c] + dp[i-1][j][k][c]) % Q;
dp[i][j][k][c] = (t1 + t2) % Q;
}
}
}
}
return dp[N][M][K][maxnC-1];
}
int main(){
scanf("%d%d%d", &N, &M, &K);
for (int i = 1; i <= N; i++)
for (int j = 1; j <= M; j++)
scanf("%d", &C[i][j]);
printf("%d", solve());
return 0;
}
!!注意:
注意向C数组读入值,以及遍历位置时,i和j的下标都是从1开始!因为要留一圈作为“边界”。(否则在真正的数组边界处计算时可能会出现越界的情况,会读到很奇怪的数字)。注意k遍历应当从0开始,因为最初包中没有宝物。
注释:
c表示背包内单个物品的最大价值,最内层循环是用来遍历背包内单个物品的最大价值的可能取值。
当i == 1且j == 1时,表示当前格子是起点。对于起点格子,只有在背包内没有物品(即k == 0)或者只有一个物品且该物品的价值大于当前格子的宝贝价值(即k == 1且c > C[i][j])的情况下,才能将dp[i][j][k][c]设置为1。这是因为起点格子是小明的初始位置,他手中还没有宝贝,所以只有在不拿宝贝或者拿到价值更大的宝贝时,才能满足题目要求。
对于其他格子,即当i != 1或j != 1时,根据动态规划的思路,需要考虑当前格子的宝贝价值是否大于背包内最大价值。如果大于背包内最大价值,说明小明可以选择拿起这个宝贝,此时将dp[i][j][k][c]更新为左边格子的方案数dp[i][j-1][k-1][C[i][j]]和上边格子的方案数dp[i-1][j][k-1][C[i][j]]之和(因为由题意可以往下或者往右走)。如果不大于背包内最大价值,说明小明无法拿起这个宝贝,将dp[i][j][k][c]保持为0。
这样,通过循环遍历每个格子和背包内物品的状态,计算出每个格子中手中拿了k件宝贝且最大宝贝价值为c的行动方案数。