前言
如题所言,DFS-记搜-DP是蓝桥杯比赛中最常用的几种方法,熟练掌握它们,拿奖将易如反掌。下面我们通过2014年地宫取宝问题,来一探这道题的3种不同的想法
题目描述
X 国王有一个地宫宝库。是 n×m 个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。
地宫的入口在左上角,出口在右下角。
小明被带到地宫的入口,国王要求他只能向右或向下行走。
走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
当小明走到出口时,如果他手中的宝贝恰好是 k 件,则这些宝贝就可以送给小明。
请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这k 件宝贝。
输入描述
输入一行 3 个整数,用空格分开: n , m , k ( 1 ≤ n , m ≤ 50 , 1 ≤ k ≤ 12 ) n,m,k (1≤n,m≤50,1≤k≤12) n,m,k(1≤n,m≤50,1≤k≤12)。
接下来有 n 行数据,每行有 m 个整数
C
i
(
0
≤
C
i
≤
12
)
C_i\ (0 \leq C_i \leq 12)
Ci (0≤Ci≤12)
代表这个格子上的宝物的价值。
输出描述
要求输出一个整数,表示正好取 kk 个宝贝的行动方案数。该数字可能很大,输出它对 1 0 9 + 7 10^9+7 109+7 取模的结果。
输入输出样例
示例
输入
2 2 2
1 2
2 1
输出
2
1. DFS
会超时
dfs下标从左到右依次为
x,y,当前已经取的宝物个数,所取宝物中的最大价值
#include <iostream>
using namespace std;
const int N = 60, MOD = 1e9 + 7;
int n, m, k;
int w[N][N];
int dfs(int x, int y, int u, int v) {
if (x > n || y > m || u > k) return 0; // 判界
if (x == n && y == m) { // 到达右下角
// 已经拿了k个宝物或者加上右下角的最后一个宝物共k个
if (u == k || u == k - 1 && w[x][y] > v) return 1;
else return 0;
}
if (w[x][y] > v) { // 当前位置的宝物价值比手里的大
// 捡或不捡均可
return dfs(x + 1, y, u, v) + dfs(x, y + 1, u, v)
+ dfs(x+1, y, u+1, w[x][y]) + dfs(x, y+1, u+1, w[x][y]);
} else {
// 只有不捡
return dfs(x + 1, y, u, v) + dfs(x, y + 1, u, v);
}
}
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> w[i][j];
cout << dfs(1, 1, 0, -1) % MOD << endl;
return 0;
}
2. 记忆化搜索
用一个数组存储过程中的值,避免了重复的计算,极大的减少了时间的使用,可以通过测试点
最明显的一个区别是记忆化搜索最后的答案是存储在第一个数组当中
并且它在过程中就要进行判界
#include <iostream>
#include <cstring>
using namespace std;
const int N = 60, MOD = 1e9 + 7;
int n, m, k;
int w[N][N];
int f[N][N][13][14];
int dfs(int x, int y, int u, int v) {
if (f[x][y][u][v] != -1) return f[x][y][u][v];
if (x == n && y == m) {
if (u == k || u == k-1 && w[x][y] > v) return f[x][y][u][v] = 1;
else return f[x][y][u][v] = 0;
}
long long s = 0;
if (x + 1 <= n) {
if (w[x][y] > v) s+= dfs(x+1, y, u+1, w[x][y]);
s += dfs(x+1, y, u, v);
}
if (y + 1 <= m) {
if (w[x][y] > v) s+= dfs(x, y+1, u+1, w[x][y]);
s += dfs(x, y+1, u, v);
}
return f[x][y][u][v] = s % MOD;
}
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> w[i][j];
w[i][j]++;
}
memset(f, -1, sizeof f);
dfs(1, 1, 0, 0); // 0 = -1
cout << f[1][1][0][0] << endl;
return 0;
}
3. DP
DP则是这道题的最佳解法,如果已经想出前面两种方法,稍加改变就能得到DP。由于我们平常的DP多是一维至二维,所以在到达4维的时候难免会产生怀疑,但还是要坚定地往下做。
- 状态表示
f[i][j][u][v]
:表示到达(i,j)位置时,所拿宝物数量为u个,最大价值为v的合法的方案数 - 状态计算:由于题目限定,只能往右或者往下,所以我们可以根据这个将集合分为两大类。往右时,同时位置上的宝物大于手中的,可以选择取或者不取,为其方案数的加和;若宝物小于手中的最大值,只有不取这种情况。往下同理。
#include <iostream>
using namespace std;
const int N = 60, MOD = 1e9 + 7;
int w[N][N];
int f[N][N][13][14];
int n, m, k;
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
cin >> w[i][j];
w[i][j]++;
// 宝贝的价值可以是0, 而-1不能用于索引,因此全部+1
}
// 初始化1,1位置上的值
f[1][1][0][0] = 1; // 不取
f[1][1][1][w[1][1]] = 1; // 取
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (i == 1 && j == 1) continue;
for (int u = 0; u <= k; u++) {
for (int v = 0; v < 13; v++) {
int& val = f[i][j][u][v];
// 不取当前位置,状态由左上方转移而来,加和
val = (val + f[i-1][j][u][v]) % MOD;
val = (val + f[i][j-1][u][v]) % MOD;
if (u > 0 && w[i][j] == v) {
for (int c = 0; c < v; c++) {
val = (val + f[i-1][j][u-1][c]) % MOD;
val = (val + f[i][j-1][u-1][c]) % MOD;
}
}
}
}
}
}
// n,m,k都确定下来,枚举最后结果中所有的最大价值
int res = 0;
for (int i = 0; i < 13; i++) res = (res + f[n][m][k][i]) % MOD;
cout << res << endl;
return 0;
}