游戏背景与问题建模
官兵捉贼是中国传统的追逃游戏:若干名”官兵”合作围捕一名”贼”,贼在固定区域内移动试图逃脱。本文将其抽象为图论追捕问题(Pursuit-Evasion Problem):给定一个无向图表示活动区域,官兵和贼轮流在图上移动,官兵的目标是移动到贼所在节点或其相邻节点实现抓捕。
核心算法涉及:图的 BFS 最短路径、多智能体协同策略、以及抓捕成功条件的判定。
算法思路
图建模
将游戏区域抽象为无向图 G = (V, E),每个节点代表一个位置,边代表可通行的路径。使用邻接表存储图结构。
最短路径预计算
对每个节点运行 BFS,预计算全源最短路径,为追捕决策提供距离信息。
协同追捕策略
- 最近官兵追捕:每次由距离贼最近的官兵向贼移动一步。
- 围堵策略:预测贼的移动方向,官兵提前占据关键节点(割点、桥)封堵逃跑路线。
- 包围圈收缩:多名官兵从不同方向逼近,逐步缩小贼的活动范围。
抓捕判定
当任意官兵与贼处于同一节点,或贼的所有邻接节点均被官兵占据时,抓捕成功。
Java 完整实现
import java.util.*;
/**
* 官兵捉贼:图论追捕与最短路径算法
* 支持多官兵协同追捕、围堵策略与抓捕判定
*/
public class CopsAndRobber {
private List<Integer>[] graph; // 邻接表
private int n; // 节点数量
private int[][] dist; // 全源最短距离
/**
* 初始化图结构
* @param edges 边列表,每条边为 {u, v}
* @param n 节点数量(节点编号 0 ~ n-1)
*/
public CopsAndRobber(int[][] edges, int n) {
this.n = n;
this.graph = new ArrayList[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
for (int[] e : edges) {
graph[e[0]].add(e[1]);
graph[e[1]].add(e[0]);
}
precomputeShortestPaths();
}
/**
* 预计算全源最短路径(对每个节点运行 BFS)
*/
private void precomputeShortestPaths() {
dist = new int[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(dist[i], -1);
bfs(i);
}
}
private void bfs(int src) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(src);
dist[src][src] = 0;
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph[u]) {
if (dist[src][v] == -1) {
dist[src][v] = dist[src][u] + 1;
queue.offer(v);
}
}
}
}
/**
* 模拟追捕过程
* @param copPositions 官兵初始位置列表
* @param robberPosition 贼初始位置
* @param maxSteps 最大模拟步数
* @return 追捕结果
*/
public ChaseResult simulateChase(List<Integer> copPositions, int robberPosition, int maxSteps) {
List<Integer> cops = new ArrayList<>(copPositions);
int robber = robberPosition;
List<String> history = new ArrayList<>();
history.add(String.format("初始: 官兵=%s, 贼=%d", cops, robber));
for (int step = 1; step <= maxSteps; step++) {
// 贼移动:逃向距离所有官兵最远的邻接节点
robber = moveRobber(robber, cops);
history.add(String.format("第%d步: 贼移动到 %d", step, robber));
// 检查抓捕
if (isCaught(robber, cops)) {
history.add("抓捕成功!");
return new ChaseResult(true, step, cops, robber, history);
}
// 官兵移动:每名官兵向贼最短路径方向前进一步
for (int i = 0; i < cops.size(); i++) {
cops.set(i, moveCopTowardsRobber(cops.get(i), robber, cops));
}
history.add(String.format("第%d步: 官兵移动到 %s", step, cops));
// 再次检查抓捕
if (isCaught(robber, cops)) {
history.add("抓捕成功!");
return new ChaseResult(true, step, cops, robber, history);
}
}
history.add("达到最大步数,贼逃脱!");
return new ChaseResult(false, maxSteps, cops, robber, history);
}
/**
* 贼的移动策略:逃向距离所有官兵总和最远的邻接节点
*/
private int moveRobber(int robber, List<Integer> cops) {
List<Integer> neighbors = graph[robber];
if (neighbors.isEmpty()) return robber;
int bestNode = robber;
int maxMinDist = -1;
for (int next : neighbors) {
// 计算该节点到最近官兵的距离
int minDist = Integer.MAX_VALUE;
for (int cop : cops) {
if (dist[cop][next] != -1) {
minDist = Math.min(minDist, dist[cop][next]);
}
}
// 选择使"到最近官兵距离"最大的节点
if (minDist > maxMinDist) {
maxMinDist = minDist;
bestNode = next;
}
}
return bestNode;
}
/**
* 官兵移动策略:沿最短路径向贼移动一步,避免碰撞
*/
private int moveCopTowardsRobber(int cop, int robber, List<Integer> otherCops) {
if (cop == robber) return cop;
List<Integer> neighbors = graph[cop];
int bestNode = cop;
int minDist = dist[cop][robber];
for (int next : neighbors) {
// 避免两名官兵移动到同一位置
if (otherCops.contains(next) && next != cop) continue;
if (dist[next][robber] != -1 && dist[next][robber] < minDist) {
minDist = dist[next][robber];
bestNode = next;
}
}
return bestNode;
}
/**
* 判断贼是否被抓捕
* 条件:贼与某官兵同节点,或贼的所有邻接节点都被官兵占据
*/
private boolean isCaught(int robber, List<Integer> cops) {
// 同节点抓捕
if (cops.contains(robber)) return true;
// 围堵抓捕:所有邻接节点都被官兵占据
List<Integer> neighbors = graph[robber];
if (neighbors.isEmpty()) return true; // 无处可逃
for (int neighbor : neighbors) {
if (!cops.contains(neighbor)) {
return false;
}
}
return true;
}
/**
* 计算最少需要多少名官兵才能确保抓捕
* 简化为:覆盖所有与贼距离为1的节点所需的最少官兵数
*/
public int minCopsNeeded(int robberPos) {
return graph[robberPos].size();
}
/**
* 寻找图上的关键节点(割点),用于围堵策略
*/
public List<Integer> findArticulationPoints() {
boolean[] visited = new boolean[n];
int[] disc = new int[n]; // 发现时间
int[] low = new int[n]; // 最早可达祖先
int[] parent = new int[n];
Arrays.fill(parent, -1);
List<Integer> articulationPoints = new ArrayList<>();
boolean[] isAp = new boolean[n];
int[] time = {0};
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfsArticulation(i, visited, disc, low, parent, isAp, time);
}
}
for (int i = 0; i < n; i++) {
if (isAp[i]) articulationPoints.add(i);
}
return articulationPoints;
}
private void dfsArticulation(int u, boolean[] visited, int[] disc, int[] low,
int[] parent, boolean[] isAp, int[] time) {
visited[u] = true;
disc[u] = low[u] = ++time[0];
int children = 0;
for (int v : graph[u]) {
if (!visited[v]) {
children++;
parent[v] = u;
dfsArticulation(v, visited, disc, low, parent, isAp, time);
low[u] = Math.min(low[u], low[v]);
if (parent[u] == -1 && children > 1) {
isAp[u] = true;
}
if (parent[u] != -1 && low[v] >= disc[u]) {
isAp[u] = true;
}
} else if (v != parent[u]) {
low[u] = Math.min(low[u], disc[v]);
}
}
}
// 结果封装
public static class ChaseResult {
public final boolean caught;
public final int steps;
public final List<Integer> finalCopPositions;
public final int finalRobberPosition;
public final List<String> history;
public ChaseResult(boolean caught, int steps, List<Integer> cops,
int robber, List<String> history) {
this.caught = caught;
this.steps = steps;
this.finalCopPositions = cops;
this.finalRobberPosition = robber;
this.history = history;
}
}
public static void main(String[] args) {
// 构建一个 4x4 网格图(节点编号 0-15)
List<int[]> edges = new ArrayList<>();
int rows = 4, cols = 4;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int id = r * cols + c;
if (c + 1 < cols) edges.add(new int[]{id, id + 1}); // 右
if (r + 1 < rows) edges.add(new int[]{id, id + cols}); // 下
}
}
CopsAndRobber game = new CopsAndRobber(edges.toArray(new int[0][]), rows * cols);
System.out.println("=== 官兵捉贼模拟 ===");
List<Integer> cops = Arrays.asList(0, 3);
int robber = 15;
ChaseResult result = game.simulateChase(cops, robber, 20);
System.out.println("抓捕结果: " + (result.caught ? "成功" : "失败"));
System.out.println("使用步数: " + result.steps);
System.out.println("历史记录:");
for (String h : result.history) {
System.out.println(" " + h);
}
System.out.println("\n割点(关键围堵位置): " + game.findArticulationPoints());
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 建图 | O(V + E) | O(V + E) |
| 全源最短路径 | O(V × (V + E)) | O(V²) |
| 单步追捕模拟 | O(V) | O(V) |
| 割点检测(Tarjan) | O(V + E) | O(V) |
图上的追捕问题与图搜索博弈(Graph Searching Game)密切相关。对于一般图,确定最少需要多少名搜索者才能捕获 fugitive 是一个 PSPACE-完全问题。本文实现的启发式策略在实际场景中表现良好。
扩展思考
- 信息不完全博弈:官兵不知道贼的精确位置,只能根据目击报告更新概率分布,使用贝叶斯推断。
- 多贼场景:多名贼分散逃跑,官兵需要分组追捕,可建模为最小费用最大流问题。
- 带权图:不同道路通行速度不同,使用 Dijkstra 替代 BFS 计算最短路径。
- 视野限制:官兵和贼只能看到一定范围内的节点,引入局部感知与巡逻策略。