ch07 题目参考思路
ch07 - 深度优先搜索
滑雪
-
知识点:路径问题、最优化问题、记忆化搜索
-
思路:
- 本题是路径问题,简单的想法是 dfs 搜索所有合法路径,最长的就是答案。但一个网格中的路径数量是指数级的,这样做会超时。
- 走到一个位置 (x, y) 时,实际上只关心 (x, y) 往后还能走多长,而不是对于所有到达 (x, y) 的路径,都要继续往后搜索所有走法。
- dfs(x, y):从 (x, y) 出发,往后最多还能走多长。
- 下一步有 4 种选择,枚举 (x, y) 下一步能到达的位置 (cx, cy),所有选择对应的结果取最大值就是 dfs(x, y) 的结果。
- 要计算 ( x , y ) → ( c x , c y ) → ⋯ (x, y)\to (cx, cy)\to \cdots (x,y)→(cx,cy)→⋯ 的最大长度,首先有 ( x , y ) (x, y) (x,y) 这一个位置,再调用 dfs(cx, cy) 得到 ( c x , c y ) → ⋯ (cx,cy)\to \cdots (cx,cy)→⋯ 的最大长度,所以这种选择对应的结果是 1 + dfs(cx, cy) 。
- 用记忆化搜索,f[x][y] 记录 dfs(x, y) 的结果,只要 f[x][y] 已经算过了,再次搜索到 dfs(x, y) 时可以直接返回结果。
- 每一个状态 (x, y) 只需要被计算一次,时间复杂度 O ( R C ) O(RC) O(RC) 。
-
代码:
int dfs(int x, int y) { if (f[x][y]) return f[x][y]; f[x][y] = 1; // 从(x,y)出发,最少也有它本身一个位置的长度 for (int i = 0; i < 4; i++) { int cx = x + dx[i], cy = y + dy[i]; if (从(x, y)走到(cx,cy)不合法) continue; f[x][y] = max(f[x][y], dfs(cx, cy) + 1); // 从不同走法中取最长的 } return f[x][y]; }
-
易错点:注意最长的路径不一定是从左上角出发的,根据 dfs(x, y) 的功能,思考 main() 函数怎么写才能得到正确答案。
水桶装水
-
知识点:DFS 解决判断问题、状态标记优化
-
思路:
- dfs(a, b):当前第一个桶的水量是 a,第二个桶的水量是 b,能否达成目标。
- 只要 (a, b) 的后继状态中有一个能达成目标,那么 (a, b) 也能达成目标。根据题目的操作,列举所有后继状态:
- 例如装满第一个桶,那么后继状态是 (x, b)。
- 例如要把第一个桶的水倒入第二个桶,先计算要倒过去的水量 d,取决于第一个桶“能倒出多少水”,以及第二个桶“能接收多少水”,d = min(a, y - b),后继状态是 (a - d, b + d) 。
- 分别对两个水桶执行三种操作,总共有六种后继状态。
- 水倒来倒去,有可能多次搜索到同一个状态,需要用数组标记搜索过的状态。
-
代码:
bool dfs(int a, int b) { if (a + b == z) return true; if (vis[a][b]) return false; vis[a][b] = true; // 第一个桶往第二个桶倒水,转移水量d1。第二个桶往第一个桶倒水,转移水量d2。 int d1 = min(a, y - b), d2 = min(b, x - a); // 思考如何列举六种后继状态并返回结果 }
Mother’s Milk
-
知识点:DFS 解决判断问题、状态标记优化
-
思路:
- 需要搜索的状态比较明显是三个桶当前各自的牛奶量。
- 在搜索过程中,用数组将能达到的状态标记为 true,最后枚举 a 桶为空的所有状态,对于能达到的状态,从小到大输出 c 桶所剩量即可。
- dfs 过程中,从哪个桶倒到哪个桶有多种可能,手动列举比较麻烦,也容易写错,可以两层循环枚举。
-
代码:
bool f[N][N][N]; int c[3], a[3]; // c[i]表示第i个桶的容量,a[i]表示当前状态第i个桶的牛奶量 // 从第i个桶倒到第j个桶 void pour(int i, int j) { int d = min(a[i], c[j] - a[j]); a[i] -= d; a[j] += d; } // 当前搜索状态是(a[0], a[1], a[2]) void dfs() { if (f[a[0]][a[1]][a[2]]) return ; // 记忆化 f[a[0]][a[1]][a[2]] = true; int b[3]; memcpy(b, a, sizeof(a)); // b用来临时储存a数组的值 for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (i == j) continue; pour(i, j); dfs(); memcpy(a, b, sizeof(b)); // pour()中a数组的值被修改,此处要改回来 } } } int main() { cin >> c[0] >> c[1] >> c[2]; a[2] = c[2]; dfs(); for (int i = 0; i <= c[2]; i++) { // c桶所剩量 for (int j = 0; j <= c[2] - i; j++) { // b桶所剩量 if (f[0][j][i]) { cout << i << " "; break; } } } return 0; }
Cows on Skates
-
知识点:网格路径问题、DFS 搜索优化
-
思路:
- 本题属于需要记录路径的网格路径问题,但网格规模较大,路径数量太多,罗列所有路径会超时。
- 优化1:题目中路径可以走重复位置,但仍然可以限制同一条路径不允许走重复位置,因为对于“走到终点”这个目标来说,绕圈是没有意义的。
- 优化2:如果搜索过一个位置 (x, y) ,这个位置无法到终点,那么其他路径也无需再尝试这个位置了。
- 因为 (x, y) 无法到终点分为 2 种情况:一是被障碍挡住,那么显然这个位置没有前途,其他路径也无需走这里;二是被路径中的其他位置 (x1, y1) 挡住,那么 (x1, y1) 比 (x, y) 更接近终点,应该回退到 (x1, y1) 去搜索,其他路径也无需搜索这个比 (x1, y1) 更劣的位置。
- 所以不是用 inPath[x][y] 标记 (x, y) 有没有在当前路径中,可以用 vis[x][y] 标记有没有搜索过 (x, y) 这个位置,这样可以保证每个位置只被搜索一次。
-
代码:
char ch[N][N]; // 输入的迷宫 int n, m; // 迷宫的行数和列数 struct P{ int x, y; } path[N*N]; // 记录状态(路径)的数组 bool vis[N][N]; // vis[x][y]标记(x,y)这个位置有没有搜索过 // (x,y)是当前状态(路径)的末尾位置,len是当前路径的长度 void dfs(int x, int y, int len) { if (vis[x][y]) return ; vis[x][y] = true; if (x == n && y == m) { // 输出结果 return ; } for (int i = 0; i < 4; i++) { int cx = x + dx[i], cy = y + dy[i]; if (cx < 1 || cx > n || cy < 1 || cy > m || ch[cx][cy] != '.') continue; path[len+1] = {cx, cy}; // 新位置放入路径中 dfs(cx, cy, len + 1); } } int main() { // 省略输入 path[1] = {1, 1}; // 注意起点放入路径中 dfs(1, 1, 1); return 0; }
Cow Travelling
-
知识点:路径问题、记忆化搜索、基本计数原理
-
思路:
- 与”滑雪“类似,不能去罗列所有路径,会超时。要有将原问题分解为子问题解决的思维,重复的子问题通过记忆化搜索避免重复计算,以此提高效率。
- 关键信息是位置和时间,那么状态中应该包含位置和时间。
- dfs(x, y, t):从 (x, y) 位置出发,经过 t 秒到达终点的路径方案数。
- 考虑下一步到达的位置 (cx, cy),有上下左右四种走法,分为四类情况统计从 (x, y) 出发的路径方案数:
- 从 (x, y) 走到 (cx, cy) 用掉了 1 秒,那么只剩下 t - 1 秒从 (cx, cy) 走到终点,调用 dfs(cx, cy, t - 1) 得到这一类走法的方案数。
- 分类统计,应用加法原理。
- 花同样的时间,走不同路径,是有可能到达同一个位置的,所以会重复搜索到同一个状态,需要记忆化搜索。
- 答案的范围:因为题目输入的时间 T 最大是 15,每一步最多有 4 种走法选择,最多走 15 步,根据乘法原理,路径方案数不会超过 4 15 4^{15} 415 ,在 int 范围内。
-
代码:
int dfs(int x, int y, int t) { if (t == 0) { // 时间用完了,到达终点说明是一条满足要求的路径,方案数为1,没到终点则是0 return (x == r2 && y == c2 ? 1 : 0); } if (f[x][y][t] != -1) return f[x][y][t]; // 方案数不会是-1,用-1作为f的初始值表示没搜索到这个状态 f[x][y][t] = 0; for (int i = 0; i < 4; i++) { int cx = x + dx[i], cy = y + dy[i]; if (cx < 1 || cx > n || cy < 1 || cy > m || ch[cx][cy] == '*') continue; f[x][y][t] += dfs(cx, cy, t - 1); // 分为往上下左右四类走法,分类用加法原理 } return f[x][y][t]; }
-
拓展:
- 为什么 dfs(x, y, t) 要定义为从 (x, y) 位置出发,经过 t 秒到达终点的路径方案数?
- 能不能定义为从起点出发,经过 t 秒到达 (x, y) 位置的方案数?思考并尝试。