1. 问题背景与核心挑战
LeetCode 913题"猫和老鼠"是一个经典的博弈论问题,模拟了猫和老鼠在无向图中的追逐游戏。题目给定一个表示图的邻接表int[][] graph,要求实现public int catMouseGame(int[][] graph)方法,返回老鼠或猫的必胜策略(如果存在),或者判断游戏是否会无限进行。
这个问题的复杂性在于:
- 状态空间庞大:需要考虑猫/鼠位置、当前移动方、步数三个维度
- 博弈双方都采取最优策略:不能简单假设某一方会犯错
- 存在平局可能:当游戏超过2n步时强制判平(n为节点数)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法思路与算法选择
2.1 动态规划与记忆化搜索
最直接的解法是使用带记忆化的深度优先搜索(DFS):
java复制int[][][] memo; // 记忆化数组:mousePos, catPos, turn
public int catMouseGame(int[][] graph) {
int n = graph.length;
memo = new int[n][n][2]; // 第三维:0鼠回合,1猫回合
return dfs(graph, 1, 2, 0);
}
2.2 状态转移分析
每个状态有三种可能结果:
- 鼠胜(返回值1):鼠到达0号节点
- 猫胜(返回值2):猫抓到鼠(位置相同)
- 平局(返回值0):步数超过2n
状态转移规则:
java复制int dfs(int[][] graph, int mouse, int cat, int turn) {
if (turn == 2 * graph.length) return 0;
if (mouse == 0) return 1;
if (mouse == cat) return 2;
if (memo[mouse][cat][turn % 2] != 0)
return memo[mouse][cat][turn % 2];
// 当前玩家选择最优策略
if (turn % 2 == 0) { // 鼠回合
boolean canDraw = false;
for (int next : graph[mouse]) {
int res = dfs(graph, next, cat, turn + 1);
if (res == 1) return memo[mouse][cat][0] = 1;
if (res == 0) canDraw = true;
}
return memo[mouse][cat][0] = canDraw ? 0 : 2;
} else { // 猫回合
boolean canDraw = false;
for (int next : graph[cat]) {
if (next == 0) continue; // 猫不能进洞
int res = dfs(graph, mouse, next, turn + 1);
if (res == 2) return memo[mouse][cat][1] = 2;
if (res == 0) canDraw = true;
}
return memo[mouse][cat][1] = canDraw ? 0 : 1;
}
}
3. 优化策略与实现细节
3.1 拓扑排序逆向推导
更高效的解法是使用拓扑排序+BFS的逆向推导:
- 确定所有基础状态(立即获胜/失败的状态)
- 逆向推导哪些状态必然导致胜利/失败
- 使用队列处理已知状态,更新相关前驱状态
java复制public int catMouseGame(int[][] graph) {
int n = graph.length;
int[][][] color = new int[n][n][2]; // 0:未处理 1:鼠胜 2:猫胜
int[][][] degree = new int[n][n][2];
// 初始化degree(每个状态的未处理子状态数)
for (int m = 0; m < n; ++m) {
for (int c = 0; c < n; ++c) {
degree[m][c][0] = graph[m].length;
degree[m][c][1] = graph[c].length;
for (int x : graph[c]) if (x == 0) degree[m][c][1]--;
}
}
// 队列存储已确定状态
Queue<int[]> queue = new LinkedList<>();
// 基础状态:鼠在0或猫抓到鼠
for (int i = 0; i < n; ++i) {
for (int t = 0; t < 2; ++t) {
color[0][i][t] = 1;
queue.add(new int[]{0, i, t});
if (i != 0) {
color[i][i][t] = 2;
queue.add(new int[]{i, i, t});
}
}
}
// BFS处理队列
while (!queue.isEmpty()) {
int[] node = queue.poll();
int m = node[0], c = node[1], t = node[2];
int result = color[m][c][t];
// 遍历所有可能的前驱状态
if (t == 1) { // 前一步是鼠移动
for (int prev : graph[m]) {
if (color[prev][c][0] != 0) continue;
if (result == 1) { // 鼠有必胜策略
color[prev][c][0] = 1;
queue.add(new int[]{prev, c, 0});
} else if (--degree[prev][c][0] == 0) {
color[prev][c][0] = 2;
queue.add(new int[]{prev, c, 0});
}
}
} else { // 前一步是猫移动
for (int prev : graph[c]) {
if (prev == 0) continue;
if (color[m][prev][1] != 0) continue;
if (result == 2) { // 猫有必胜策略
color[m][prev][1] = 2;
queue.add(new int[]{m, prev, 1});
} else if (--degree[m][prev][1] == 0) {
color[m][prev][1] = 1;
queue.add(new int[]{m, prev, 1});
}
}
}
}
return color[1][2][0];
}
3.2 复杂度分析
- 时间复杂度:O(n^3),其中n是图的节点数
- 空间复杂度:O(n^2)用于存储状态
4. 常见问题与调试技巧
4.1 无限循环问题
当使用DFS时容易忽略步数限制,导致栈溢出。关键检查点:
- 必须设置turn >= 2n时的终止条件
- 记忆化数组需要包含turn的奇偶信息(因为移动方由turn决定)
4.2 状态表示错误
常见错误包括:
- 忘记猫不能进入0号节点
- 错误处理回合交替逻辑(鼠/猫轮流移动)
- 记忆化数组维度设置错误(需要区分鼠/猫回合)
4.3 性能优化技巧
- 使用位运算压缩状态(当n较小时)
- 预处理每个节点的合法移动
- 对于对称图可以进行状态合并
5. 测试用例设计
完整验证需要覆盖以下场景:
java复制// 简单直线图:鼠必胜
int[][] graph1 = {{1}, {0,2}, {1}};
assert catMouseGame(graph1) == 1;
// 猫必胜情况
int[][] graph2 = {{1,2}, {0,2}, {0,1}};
assert catMouseGame(graph2) == 2;
// 平局情况(环状图)
int[][] graph3 = {{1}, {0}};
assert catMouseGame(graph3) == 0;
// 复杂图测试
int[][] graph4 = {{2,3}, {3}, {0,1}, {1,2}};
assert catMouseGame(graph4) == 1;
6. 实际应用与扩展
虽然这个问题看起来像纯理论题目,但它有重要的实际应用价值:
- 网络安全领域:模拟攻击者(鼠)和防御者(猫)在网络拓扑中的对抗
- 机器人路径规划:多智能体系统中的避障与追逐策略
- 游戏AI开发:设计智能NPC的决策算法
对于想进一步挑战的开发者,可以考虑以下变种:
- 增加障碍物或特殊节点
- 引入移动速度差异
- 考虑不完全信息博弈(部分节点不可见)
- 多猫或多鼠场景
这个题目很好地展示了如何将博弈论思想转化为实际代码,理解它对于培养系统性思维和状态空间分析能力非常有帮助。在实际编码时,建议先用小规模图手动推导状态转移,确保完全理解游戏规则后再着手实现。
