X 国王有一个地宫宝库。是 n×m 个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。
地宫的入口在左上角,出口在右下角。
小明被带到地宫的入口,国王要求他只能向右或向下行走。
走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
当小明走到出口时,如果他手中的宝贝恰好是 k 件,则这些宝贝就可以送给小明。
请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这 k 件宝贝。
输入一行 3 个整数,用空格分开:n,m,k (1≤n,m≤50,1≤k≤12)。
接下来有 n 行数据,每行有 m 个整数 Ci (0≤Ci≤12) 代表这个格子上的宝物的价值。
要求输出一个整数,表示正好取 k 个宝贝的行动方案数。该数字可能很大,输出它对 109+7 取模的结果。
#include
#include
#include
using namespace std;
// 全局常量定义
const int MOD = 1e9 + 7; // 模数取10^9+7
const int MAX_K = 13; // 最大可取宝物数+1(k≤12)
const int MAX_VAL = 14; // 价值上限(原价值0-12,+1后1-13)
int n, m, k; // 地宫行列数和目标宝物数
vector> grid; // 地宫宝物价值矩阵
// 四维记忆数组:memo[i][j][cnt][max_val] 表示在(i,j)位置,
// 已取cnt个宝物且当前最大价值为max_val的状态方案数[3](@ref)
int memo[50][50][13][14]; // 按题目最大范围50x50x12x14
// 深度优先搜索函数
// 参数:当前位置(i,j),已收集宝物数cnt,当前最大价值max_val
// 返回:从当前位置到终点的合法路径数[8](@ref)
int dfs(int i, int j, int cnt, int max_val) {
// 终点判断(右下角格子)
if (i == n-1 && j == m-1) {
int cases = 0;
// 情况1:已收集k件且不取终点[6](@ref)
if (cnt == k) cases++;
// 情况2:已收集k-1件且可取终点宝物
if (cnt == k-1 && grid[i][j] > max_val) cases++;
return cases % MOD;
}
// 记忆化查询(已计算过的状态直接返回)
if (memo[i][j][cnt][max_val] != -1)
return memo[i][j][cnt][max_val];
int res = 0;
// 不取当前格子的两种情况(向右/向下移动)
if (j+1 < m) // 向右移动合法时
res = (res + dfs(i, j+1, cnt, max_val)) % MOD;
if (i+1 < n) // 向下移动合法时
res = (res + dfs(i+1, j, cnt, max_val)) % MOD;
// 尝试取当前格子的两种情况(需满足价值递增条件)
if (grid[i][j] > max_val && cnt < k) {
int new_cnt = cnt + 1;
int new_max = grid[i][j];
// 更新状态后继续递归
if (j+1 < m)
res = (res + dfs(i, j+1, new_cnt, new_max)) % MOD;
if (i+1 < n)
res = (res + dfs(i+1, j, new_cnt, new_max)) % MOD;
}
// 记录当前状态并返回结果
return memo[i][j][cnt][max_val] = res;
}
int main() {
// 输入处理
cin >> n >> m >> k;
grid.resize(n, vector(m));
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) {
cin >> grid[i][j];
grid[i][j]++; // 价值+1处理(避免0值冲突)[3](@ref)
}
// 初始化记忆数组为-1(表示未计算状态)
memset(memo, -1, sizeof(memo));
// 从起点(0,0)开始,初始状态未取任何宝物(cnt=0,max_val=0)
cout << dfs(0, 0, 0, 0) << endl;
return 0;
}
DFS的基本思路应该是:从起点(0,0)开始,每次选择向右或向下移动,同时记录当前已经收集的宝物数量以及当前的最大价值。当到达终点时,如果收集的宝物数量正好是k,则计数加一。
不过,这样的纯递归会有很多重复计算,导致效率极低。因此,通常会使用记忆化搜索(即DFS+备忘录)来优化。记忆化搜索需要记录状态,包括当前位置(i,j)、已收集的数量cnt、当前最大价值max_val。这样可以将已经计算过的状态保存下来,避免重复计算。
在动态规划中,状态是四维的:位置(i,j)、数量c、最大价值v。同样,在记忆化DFS中,也需要同样的状态参数。因此,记忆化数组应该是四维的,或者至少包含这四个关键状态。
另外,需要注意的是,宝物的价值可能为0,但为了处理初始状态(没有拿任何宝物时,max_val可以设为-1或者0,但需要统一处理)。在之前的动态规划解法中,宝物价值被加1,以避免0的问题,可能在这里也需要类似的处理。
所以,DFS的步骤大概是这样的:
但是,直接这样实现可能会有问题,比如当max_val的值范围较大时,数组会很大。不过题目中宝物的价值最多是12,加上1之后是13,所以max_val的可能取值是0到13(初始状态设为-1,但处理后变为0)。
另外,需要处理数组越界的问题,即i和j不能超过n-1和m-1。
现在,我需要考虑如何初始化记忆化数组。由于n和m最多是50,k是12,max_val是0到13,所以记忆化数组的维度是50x50x13x14,这在C++中应该是可行的,但需要注意内存使用。不过,题目中的结果需要对1e9+7取模,所以用int或long long存储。
接下来,编写代码的大致结构:
另外,处理拿取宝物的情况时,如果当前宝物的价值大于max_val,并且cnt+1 <=k,则可以拿取,此时新的max_val变为当前宝物的价值,cnt+1。
需要注意的是,初始状态时,max_val可能是-1(如果还没有拿任何宝物),或者根据处理后的值进行调整。例如,在动态规划中,宝物价值被加1,所以初始max_val可以是0,这样当第一次拿的宝物价值为0(处理后为1)时,可以正确比较。
因此,在DFS中,宝物价值也应该加1,这样初始的max_val可以设为0(表示没有拿任何宝物),当第一次拿的时候,只要当前宝物的价值(已加1)>0即可。
所以,在代码中,输入的宝物价值需要先加1处理,类似于动态规划的做法。
现在,综合这些思路,可以开始编写代码框架:
定义memo[i][j][cnt][max_val],表示在(i,j)位置,已经拿了cnt个宝物,当前最大价值是max_val时的路径数。
递归函数的大致逻辑:
int dfs(int i, int j, int cnt, int max_val) {
if (i == n-1 && j == m-1) {
return cnt == k ? 1 : 0;
}
if (memo[i][j][cnt][max_val] != -1) {
return memo[i][j][cnt][max_val];
}
int res = 0;
// 向右走
if (j+1 < m) {
// 不拿下一个位置的宝物
res += dfs(i, j+1, cnt, max_val);
res %= MOD;
// 拿下一个位置的宝物,前提是满足条件
int next_val = grid[i][j+1];
if (cnt < k && next_val > max_val) {
res += dfs(i, j+1, cnt+1, next_val);
res %= MOD;
}
}
// 向下走
if (i+1 < n) {
// 不拿
res += dfs(i+1, j, cnt, max_val);
res %= MOD;
// 拿
int next_val = grid[i+1][j];
if (cnt < k && next_val > max_val) {
res += dfs(i+1, j, cnt+1, next_val);
res %= MOD;
}
}
memo[i][j][cnt][max_val] = res;
return res;
}
不过,这样的写法可能有问题,因为当移动到下一个位置时,处理的是下一个位置的宝物。例如,当向右走到(i,j+1)时,是否拿的是grid[i][j+1]的宝物。所以,在当前位置处理的是当前位置的宝物,或者下一个位置的?
这里可能存在逻辑错误。正确的做法应该是:当移动到下一个位置(比如向右走到(i,j+1)),此时处理的是该位置的宝物。因此,在到达(i,j)时,是否拿取的是grid[i][j]的宝物?
或者,可能在递归时处理的是当前所在位置的宝物。例如,小明在移动到(i,j)时,可以选择是否拿取这里的宝物,然后继续移动。
所以,可能需要重新调整递归的逻辑:当小明到达(i,j)时,处理该位置的宝物,然后移动到右边或下边。
这样,初始调用应该是从起点(0,0)开始,处理是否拿取起点处的宝物,然后递归到右边或下边。
因此,递归函数的结构可能需要调整:
在到达(i,j)时,有两种情况:已经拿了这里的宝物,或者没有拿。因此,在递归时,需要考虑在当前位置是否拿取宝物,然后处理移动。
这可能更符合问题的逻辑。例如,小明走到(i,j)时,可以选择是否拿这里的宝物(如果满足条件),然后继续向右或向下走。
因此,递归函数的结构可能需要重新设计:
函数参数包括当前位置(i,j),已经拿的数量cnt,当前最大价值max_val。当处理当前位置时,可以选择拿或者不拿,然后移动。
例如,在函数内部:
但这样的话,可能需要记录是否已经在该位置拿过宝物,但题目中每个格子只能经过一次,所以每个格子只能处理一次是否拿取,不能在之后返回时再拿。
因此,正确的处理流程是:当小明第一次到达(i,j)时,决定是否拿取该宝物,然后继续移动,不能再回来拿。
因此,递归函数的结构应该是这样的:
当到达(i,j)时,首先处理是否拿取该位置的宝物,然后向右或向下移动。
因此,递归函数的参数应该包括当前位置(i,j),已拿数量cnt,当前最大价值max_val。在到达该位置时,处理拿或者不拿的选择,然后递归到下一个位置。
这样,初始调用应该是在起点(0,0),此时可以选择拿或者不拿起点的宝物,然后递归到右边或下边。
因此,递归函数的逻辑应该调整为:
在(i,j)位置时:
这样,递归的处理应该是在每个位置处理拿或不拿的选择,然后再移动。而不是在移动之后处理下一个位置的宝物。
这可能更正确,因为小明在走到某个格子时,才决定是否拿该格子的宝物,而不是在移动前处理下一个位置的宝物。
所以,正确的递归步骤应该是:
到达(i,j)时:
同时,需要考虑到达终点时的处理:必须在终点处完成所有选择,即是否拿终点的宝物,使得最终数量正好是k。
因此,递归函数的终止条件是当i ==n-1且j ==m-1时,处理完该位置的拿或不拿后,检查cnt是否等于k。
这样,整个递归的结构需要调整。
例如:
int dfs(int i, int j, int cnt, int max_val) {
// 处理当前格子是否拿取
if (i == n-1 && j == m-1) {
// 到达终点,检查当前处理后的cnt是否等于k
// 这里需要处理是否拿终点处的宝物
int ans =0;
// 不拿的情况
if (cnt ==k) {
ans +=1;
}
// 拿的情况,如果满足条件
if (cnt
ans +=1;
}
return ans % MOD;
}
if (memo[i][j][cnt][max_val] != -1) {
return memo[i][j][cnt][max_val];
}
int res =0;
// 处理当前格子是否拿取
// 选择不拿
// 然后移动
if (j+1
res %= MOD;
}
if (i+1
res %= MOD;
}
// 这里可能写错了,应该是i+1,j?
// 修正:
if (i+1
res %= MOD;
}
// 选择拿当前格子宝物
if (grid[i][j] > max_val && cnt
但这样处理的话,在到达每个位置时,会有两种选择:拿或者不拿当前宝物,然后继续移动。这样,在递归中,每个位置的处理分为两种情况,导致状态数翻倍。但根据题目要求,小明在走到某个格子时,可以选择拿或者不拿,但一旦离开该格子就不能再回来拿。
因此,这样的结构是正确的。而初始调用应该是从起点开始,处理拿或不拿起点的宝物,然后递归。
但此时,初始调用应该如何?例如,小明在起点(0,0)处,可以选择拿或不拿:
所以,最终的答案应该是:
dfs(0,0,0,0) // 不拿起点宝物的情况,加上 dfs(0,0,1, grid[0][0]) // 拿起点宝物的情况?
或者,在dfs函数内部处理是否拿取当前宝物?
这需要重新设计递归函数,使其处理的是在当前位置的拿或不拿的选择,而不是在移动之后处理下一个位置的选择。
这可能更合理。例如,在函数中,当处理到(i,j)时,首先处理是否拿取这里的宝物,然后递归到右边和下面的格子。
此时,递归函数的参数是当前位置(i,j),已经拿的数量cnt,当前最大价值max_val,表示的是到达该位置时已经做的选择(是否拿取该位置的宝物之前的状态)。
例如,当小明到达(i,j)时,还没有决定是否拿这里的宝物,所以函数内部处理是否拿取,然后移动。
或者,参数表示的是已经处理完该位置的拿取状态,即到达(i,j)时,已经完成了拿或不拿的选择,此时的状态是cnt和max_val。然后,递归到右边和下面的格子。
这可能需要将递归函数分为两种情况:在处理完当前位置的选择后,移动。
例如,函数dfs(i,j,cnt,max_val)表示的是小明在(i,j)位置,已经处理过是否拿这里的宝物,现在要移动到下一个格子。这可能不太对。
或者,另一种设计方式是:当小明移动到(i,j)时,处理是否拿这里的宝物,然后继续移动。因此,函数参数表示的是到达(i,j)时的状态(比如,还没有拿这里的宝物),然后在函数内部处理拿或者不拿的选择,再移动。
这可能会更复杂,因为每个位置的处理分为两种情况:拿或者不拿。
或者,将函数参数设计为到达(i,j)时的状态,包括是否已经拿取了该位置的宝物。但这会增加状态参数,例如一个布尔标志,表示是否已经处理过该位置的拿取。这会导致状态数进一步增加,可能不可行。
因此,可能需要重新考虑递归函数的设计,使其在到达(i,j)时,处理拿或不拿的选择,然后移动。
正确的做法应该是:小明到达(i,j)格子,此时可以决定是否拿该格子的宝物,前提是满足条件。然后,无论是否拿取,都必须向右或向下移动,直到终点。
因此,递归函数的参数应该是在到达(i,j)时的状态(即已经拿的数目cnt和当前最大max_val),然后处理是否拿取该格子的宝物,然后移动到下一个格子。
例如:
在函数dfs(i,j,cnt,max_val)中:
需要注意的是,每个格子只能处理一次拿取决策,即小明在第一次到达该格子时决定是否拿取,之后不能返回。
因此,递归函数的正确逻辑应该是:
当小明到达(i,j)时,处理该格子的拿取选择,然后移动。
所以,递归函数的调用应该是在处理完当前格子之后,移动到下一个格子。例如:
int dfs(int i, int j, int cnt, int max_val) {
// 如果已经处理过这个状态,直接返回
if (memo[i][j][cnt][max_val] != -1) {
return memo[i][j][cnt][max_val];
}
// 当前格子是否处理拿取?
// 不拿的情况:此时不改变cnt和max_val,直接移动
// 拿的情况:如果满足条件,更新cnt和max_val,然后移动
int res =0;
// 先处理不拿当前格子的情况,直接移动
// 但需要移动到右边或下边
// 但此时需要移动到下一个格子,所以在移动之后处理下一个格子的选择?
// 不,这样可能不正确,因为当前格子的处理应该是在该格子停留时进行的选择,而不是移动后的格子。
// 正确的做法是,在当前位置处理是否拿取,然后移动到下一个位置时处理下一个位置的选择。
// 因此,当前函数处理的是到达(i,j)后的选择,然后移动到下一个位置。
// 因此,需要先处理是否拿取当前格子,然后递归到下一个位置。
// 所以,处理当前格子的拿取选择:
// 情况一:不拿当前格子
int new_cnt = cnt;
int new_max = max_val;
// 此时,移动到右边或下边,递归处理下一个格子
if (j < m-1) { // 可以向右
res += dfs(i, j+1, new_cnt, new_max);
res %= MOD;
}
if (i < n-1) { // 可以向下
res += dfs(i+1, j, new_cnt, new_max);
res %= MOD;
}
// 情况二:拿当前格子,如果满足条件
if (grid[i][j] > new_max && new_cnt new_max && new_cnt
但这样的处理可能比较复杂,尤其是在终点处的判断。因为当i,j是终点时,j+1和i+1都会越界,所以在移动到下个位置时会直接返回0,而真正的判断需要在处理当前格子时进行。
因此,在递归函数中,当处理到终点(i,j)时,无论是选择拿还是不拿,都需要立即判断是否满足条件,并返回相应的结果,而不是继续移动。
因此,递归函数的终止条件应该是在处理完当前格子(终点)后,检查是否满足k件宝物。
因此,正确的递归终止条件应该是在处理当前位置是否为终点时,无论是拿还是不拿,都要计算可能的情况。
这可能需要在递归函数的开始部分处理:
int dfs(int i, int j, int cnt, int max_val) {
// 检查是否已经缓存
if (memo[i][j][cnt][max_val] != -1) {
return memo[i][j][cnt][max_val];
}
// 处理终点情况
if (i == n-1 && j == m-1) {
int ans =0;
// 不拿该格子的情况
if (cnt ==k) {
ans +=1;
}
// 拿该格子的情况
if (cnt max_val && cnt+1 ==k) {
ans +=1;
}
ans %= MOD;
memo[i][j][cnt][max_val] = ans;
return ans;
}
// 非终点情况,处理拿或不拿的选择
int res =0;
// 不拿当前格子,直接移动
if (j+1 max_val && cnt
这样,当到达终点时,处理是否拿取该位置的宝物,并判断是否满足cnt==k或者cnt+1 ==k(如果是拿的情况)。
这样,递归函数的初始调用应该是:
int ans = dfs(0, 0, 0, 0);
但是,这样的话,在起点处,小明可以选择不拿或者拿。但根据上述代码,在起点处的处理会被自动处理吗?
是的,因为在递归函数中,当调用dfs(0,0,0,0)时,会处理是否拿取(0,0)的宝物。例如,在非终点的情况下,处理不拿和拿的情况:
这样,初始调用只需要一次,即dfs(0,0,0,0),因为递归函数内部处理了是否拿取当前格子的宝物。
但是,这样的处理是否正确?例如,小明在起点处选择不拿,那么后面的移动是否还能拿其他宝物?是的,因为max_val仍然是0,后面遇到比0大的宝物可以拿。
但如果小明在起点处选择拿,那么cnt变为1,max_val变为grid[0][0]。
这样,递归函数的设计是正确的。
现在,关于记忆化数组的初始化,需要将所有值初始化为-1,表示未访问过。在C++中,可以用memset或fill函数进行初始化。
此外,考虑到宝物的价值已经被加1处理,max_val的取值范围是0到13(因为原来的Ci最大是12,加1后最大是13)。
因此,记忆化数组的维度应该是n x m x (k+1) x 14。例如,在C++中,可以定义为:
int memo[50][50][13][14]; // n,m <=50; k<=12; max_val <=13.
但动态定义可能更灵活。或者,使用vector来定义四维数组。
例如:
vector
vector
vector
vector
)
)
);
这样,在每次测试用例时,重新初始化memo数组为-1。
综上,结合以上思路,可以编写出DFS+记忆化的代码。但需要注意,这种方法在n和m较大时,可能会超时,因为状态数是50 * 50 * 12 * 14=420,000,每个状态需要计算多次,但可能还是可以接受的,尤其是在题目给定的时间限制下。
最后,需要将输入的宝物价值加1,并将初始max_val设为0(表示未拿任何宝物时的状态)。