引言
青蛙过河是一款经典的益智游戏:青蛙需要从河岸一侧通过若干荷叶跳到对岸。每片荷叶存在一定的承重概率,青蛙落脚后荷叶可能沉没。玩家需要在有限的生命值内,选择最优的跳跃路径到达对岸。本文用Java实现该游戏,重点讲解蒙特卡洛模拟(Monte Carlo Simulation)评估各路径的成功概率,并基于概率最大化策略决策最优跳跃序列。
核心算法思路
1. 问题建模
将过河场景抽象为图结构:
– 节点:每片荷叶及两岸
– 边:青蛙可跳跃的距离范围内的荷叶对
– 权重:荷叶沉没概率(动态变化)
青蛙每次跳跃后,落脚荷叶以一定概率沉没,沉没后该节点永久失效。目标是找到期望成功率最高的路径。
2. 蒙特卡洛模拟
蒙特卡洛方法通过大量随机采样估计复杂系统的统计特性。对于青蛙过河:
– 对候选路径进行 N 次独立模拟
– 每次模拟中,按荷叶沉没概率随机决定是否沉没
– 统计成功到达对岸的次数比例,作为该路径的期望成功率
3. 概率最优策略
对每个可达的下一跳目标,用蒙特卡洛模拟评估从该位置到终点的成功概率,选择概率最大的方向。
Java 实现
import java.util.*;
/**
* 青蛙过河 - 蒙特卡洛模拟与概率最优跳跃策略
*/
public class FrogCrossingRiver {
// 荷叶节点
static class LilyPad {
int id;
double x, y; // 坐标位置
double sinkProb; // 单次踩踏后沉没概率
boolean sunk = false; // 是否已沉没
LilyPad(int id, double x, double y, double sinkProb) {
this.id = id; this.x = x; this.y = y; this.sinkProb = sinkProb;
}
LilyPad copy() {
LilyPad p = new LilyPad(id, x, y, sinkProb);
p.sunk = sunk;
return p;
}
}
// 游戏状态
static class GameState {
List<LilyPad> pads;
int frogPos; // 当前所在荷叶id,-1表示左岸,-2表示右岸
GameState(List<LilyPad> pads, int frogPos) {
this.pads = pads; this.frogPos = frogPos;
}
GameState copy() {
List<LilyPad> np = new ArrayList<>();
for (LilyPad p : pads) np.add(p.copy());
return new GameState(np, frogPos);
}
}
final double maxJumpDist; // 青蛙最大跳跃距离
final int monteCarloRuns; // 蒙特卡洛模拟次数
final Random rand = new Random(42);
public FrogCrossingRiver(double maxJumpDist, int monteCarloRuns) {
this.maxJumpDist = maxJumpDist;
this.monteCarloRuns = monteCarloRuns;
}
// 计算两点距离
double dist(LilyPad a, LilyPad b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
/**
* 获取从当前位置可跳跃的所有目标
*/
List<Integer> getReachable(GameState state) {
List<Integer> targets = new ArrayList<>();
double cx, cy;
if (state.frogPos == -1) { // 左岸
cx = 0; cy = 0;
} else {
LilyPad cur = state.pads.get(state.frogPos);
if (cur.sunk) return targets;
cx = cur.x; cy = cur.y;
}
// 右岸作为目标
double rightBankX = 100.0;
if (Math.abs(rightBankX - cx) <= maxJumpDist) {
targets.add(-2);
}
// 各荷叶作为目标
for (int i = 0; i < state.pads.size(); i++) {
LilyPad p = state.pads.get(i);
if (p.sunk || i == state.frogPos) continue;
if (Math.hypot(p.x - cx, p.y - cy) <= maxJumpDist) {
targets.add(i);
}
}
return targets;
}
/**
* 单次蒙特卡洛模拟:从给定状态出发,随机策略走到终点,返回是否成功
*/
boolean simulateOnce(GameState initState) {
GameState state = initState.copy();
while (true) {
List<Integer> reachable = getReachable(state);
// 如果能直接到右岸,优先尝试
if (reachable.contains(-2)) {
// 先处理当前荷叶沉没
if (state.frogPos >= 0) {
LilyPad cur = state.pads.get(state.frogPos);
if (rand.nextDouble() < cur.sinkProb) {
cur.sunk = true; // 沉没,青蛙落水失败
return false;
}
}
return true; // 成功到达右岸
}
// 过滤掉已沉没和不可达的
reachable.removeIf(id -> id == -2);
if (reachable.isEmpty()) return false;
// 随机选择下一跳(模拟中可用任意策略,这里用随机)
int next = reachable.get(rand.nextInt(reachable.size()));
// 当前荷叶可能沉没
if (state.frogPos >= 0) {
LilyPad cur = state.pads.get(state.frogPos);
if (rand.nextDouble() < cur.sinkProb) {
cur.sunk = true;
return false;
}
}
state.frogPos = next;
}
}
/**
* 蒙特卡洛评估:从当前状态选择某目标后的期望成功率
*/
double evaluateMove(GameState state, int targetId) {
int success = 0;
for (int i = 0; i < monteCarloRuns; i++) {
GameState sim = state.copy();
// 执行跳跃到targetId
if (sim.frogPos >= 0) {
LilyPad cur = sim.pads.get(sim.frogPos);
if (rand.nextDouble() < cur.sinkProb) {
continue; // 起跳就沉没,算失败
}
}
sim.frogPos = targetId;
// 目标荷叶是否立即沉没(仅在踩踏后)
if (targetId >= 0) {
LilyPad target = sim.pads.get(targetId);
if (rand.nextDouble() < target.sinkProb) {
continue; // 落脚沉没
}
}
if (targetId == -2) {
success++; // 到达右岸
} else {
if (simulateOnce(sim)) success++;
}
}
return (double) success / monteCarloRuns;
}
/**
* 决策最优下一跳
*/
int decideBestMove(GameState state) {
List<Integer> reachable = getReachable(state);
if (reachable.isEmpty()) return -3; // 无路可走
if (reachable.contains(-2)) return -2; // 能直接到对岸就直接跳
double bestProb = -1;
int bestId = -3;
for (int target : reachable) {
double prob = evaluateMove(state, target);
if (prob > bestProb) {
bestProb = prob;
bestId = target;
}
}
return bestId;
}
/**
* 完整游戏流程
*/
public boolean playGame(List<LilyPad> pads) {
GameState state = new GameState(pads, -1);
int moves = 0;
final int maxMoves = 50;
while (moves < maxMoves) {
int move = decideBestMove(state);
if (move == -3) {
System.out.println("第" + moves + "步:无路可走,失败");
return false;
}
if (move == -2) {
System.out.println("第" + moves + "步:成功到达右岸!");
return true;
}
LilyPad target = state.pads.get(move);
System.out.printf("第%d步:从%d跳到荷叶%d (坐标%.1f,%.1f,沉没概率%.2f)%n",
moves, state.frogPos, move, target.x, target.y, target.sinkProb);
state.frogPos = move;
moves++;
}
System.out.println("超过最大步数限制");
return false;
}
public static void main(String[] args) {
// 构造测试场景:河宽100,荷叶分布在中间区域
List<LilyPad> pads = new ArrayList<>();
pads.add(new LilyPad(0, 20, 10, 0.1));
pads.add(new LilyPad(1, 25, -5, 0.15));
pads.add(new LilyPad(2, 40, 8, 0.05));
pads.add(new LilyPad(3, 45, -8, 0.2));
pads.add(new LilyPad(4, 60, 5, 0.1));
pads.add(new LilyPad(5, 70, -3, 0.08));
pads.add(new LilyPad(6, 80, 7, 0.12));
pads.add(new LilyPad(7, 55, -10, 0.18));
pads.add(new LilyPad(8, 35, 12, 0.06));
pads.add(new LilyPad(9, 85, -6, 0.14));
FrogCrossingRiver game = new FrogCrossingRiver(30.0, 1000);
boolean win = game.playGame(pads);
System.out.println("游戏结果:" + (win ? "胜利" : "失败"));
}
}
算法复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 单次模拟 | O(M) | M 为平均路径长度 |
| 单步决策 | O(K × N × M) | K 为可选方向数,N 为蒙特卡洛次数 |
| 空间复杂度 | O(P) | P 为荷叶数量,用于状态复制 |
优化方向
- 重要性采样:对高沉没概率荷叶减少采样权重,聚焦关键路径
- 并行模拟:利用多线程并行执行蒙特卡洛实验,降低决策延迟
- 价值函数近似:用神经网络拟合状态价值,避免每次重复模拟
- UCT 树搜索:结合蒙特卡洛树搜索(MCTS)在动作空间上做系统性探索
总结
本文通过蒙特卡洛模拟为青蛙过河游戏提供了概率最优的跳跃决策。核心思想是将不确定环境下的路径规划转化为统计估计问题,通过大量随机采样逼近真实的成功概率。这种方法不仅适用于游戏 AI,也广泛应用于金融风控、物流路径规划等存在不确定性的决策场景。