每日算法 — 使用java实现丢手绢:环形追及与概率最优捕捉策略

丢手绢是经典的儿童围圈游戏:众人围坐成环,一人持手绢绕圈奔跑,伺机丢在某人身后,被丢者发现后起身追逐,持绢者需在被抓住前跑到对方空位坐下。本文用Java建模这一游戏的追及过程,通过环形队列与概率分析,求解最优丢绢位置与追逐策略。

游戏机制与数学建模

将n名玩家均匀排列在圆周上,相邻两人间距为1单位。持绢者跑动速度为v1,被丢者反应时间为t后起身追赶,速度为v2。持绢者需要选择丢绢位置,使得自己能在被抓住前跑回该位置的正对面空位坐下。

核心决策变量:
– 丢绢距离d:持绢者相对于目标需要跑过的弧长
– 临界条件:持绢者跑完半圈(n/2)的时间 ≤ 被丢者反应时间 + 追赶所需时间

环形追及的临界分析

持绢者沿圆周跑向空位,最短路径为半圈n/2。被丢者发现后沿相反方向追赶,双方构成环形追及问题。设圆周总长为n,则持绢者成功的充要条件为:

(n/2) / v1 < t + (n/2) / v2

整理得临界速度比:v1/v2 > n / (n + 2*v1*t)。当持绢者速度优势足够大时,任意位置均可成功;否则需要选择靠近目标的位置丢绢以缩短跑程。

Java完整实现

import java.util.*;

/**
 * 丢手绢游戏 - 环形追及与概率最优策略
 * 建模围圈奔跑的追及问题,分析最优丢绢位置
 */
public class DropTheHandkerchief {

    // ==================== 核心模型:环形玩家队列 ====================

    static class Player {
        int id;
        String name;
        double speed;      // 奔跑速度
        double reaction;   // 反应时间(秒)
        boolean hasHandkerchief; // 是否持绢
        int position;      // 在环上的位置索引

        Player(int id, String name, double speed, double reaction) {
            this.id = id;
            this.name = name;
            this.speed = speed;
            this.reaction = reaction;
            this.hasHandkerchief = false;
        }
    }

    /**
     * 环形玩家队列:用数组模拟围坐成圈的玩家
     */
    static class CircleQueue {
        Player[] players;
        int size;

        CircleQueue(int n) {
            players = new Player[n];
            size = n;
        }

        void setPlayer(int pos, Player p) {
            players[pos] = p;
            p.position = pos;
        }

        /**
         * 计算两点之间的最短弧长距离
         * 环形结构:顺时针和逆时针取较小值
         */
        int shortestDistance(int from, int to) {
            int clockwise = (to - from + size) % size;
            int counterClockwise = (from - to + size) % size;
            return Math.min(clockwise, counterClockwise);
        }

        /**
         * 从from出发沿指定方向走到to的步数
         * @param direction 1为顺时针, -1为逆时针
         */
        int distanceWithDirection(int from, int to, int direction) {
            if (direction == 1) {
                return (to - from + size) % size;
            } else {
                return (from - to + size) % size;
            }
        }
    }

    // ==================== 核心算法:追及判定与最优位置 ====================

    /**
     * 判定持绢者从target位置丢绢后能否成功逃脱
     * @param circle 环形队列
     * @param runnerId 持绢者ID
     * @param targetId 被丢绢者ID
     * @param dropDistance 在target身后多远丢绢(步数)
     * @return 逃脱成功返回true
     */
    static boolean canEscape(CircleQueue circle, int runnerId, int targetId, int dropDistance) {
        Player runner = circle.players[runnerId];
        Player target = circle.players[targetId];

        int n = circle.size;
        // 持绢者需要跑到target对面的空位,即target + n/2的位置
        int emptySeat = (target.position + n / 2) % n;
        int runDistance = circle.shortestDistance(runner.position, emptySeat);

        double runnerTime = (double) runDistance / runner.speed;

        // 被丢者反应时间 + 追赶时间
        // 被丢者沿与runner相反方向跑向emptySeat(因为runner要去那)
        int chaseDistance = circle.shortestDistance(target.position, emptySeat);
        double targetTime = target.reaction + (double) chaseDistance / target.speed;

        return runnerTime < targetTime; // 严格小于表示成功
    }

    /**
     * 为持绢者寻找最优丢绢目标:能成功逃脱且跑程最短
     * @return 最优目标ID列表(可能多个同样优)
     */
    static List<Integer> findBestTargets(CircleQueue circle, int runnerId) {
        Player runner = circle.players[runnerId];
        List<Integer> bestTargets = new ArrayList<>();
        int minRunDistance = Integer.MAX_VALUE;

        for (int i = 0; i < circle.size; i++) {
            if (i == runnerId) continue; // 不能丢给自己

            if (canEscape(circle, runnerId, i, 1)) {
                int emptySeat = (circle.players[i].position + circle.size / 2) % circle.size;
                int runDist = circle.shortestDistance(runner.position, emptySeat);

                if (runDist < minRunDistance) {
                    minRunDistance = runDist;
                    bestTargets.clear();
                    bestTargets.add(i);
                } else if (runDist == minRunDistance) {
                    bestTargets.add(i);
                }
            }
        }
        return bestTargets;
    }

    /**
     * 计算每个位置作为丢绢目标的成功概率
     * 假设目标反应时间在[minReact, maxReact]均匀分布
     */
    static Map<Integer, Double> computeSuccessProbability(
            CircleQueue circle, int runnerId, double minReact, double maxReact) {
        Map<Integer, Double> probMap = new HashMap<>();
        Player runner = circle.players[runnerId];
        int n = circle.size;

        for (int i = 0; i < n; i++) {
            if (i == runnerId) continue;
            Player target = circle.players[i];
            int emptySeat = (target.position + n / 2) % n;
            int runDist = circle.shortestDistance(runner.position, emptySeat);
            int chaseDist = circle.shortestDistance(target.position, emptySeat);

            double runnerTime = (double) runDist / runner.speed;
            // 目标成功追赶需要: reaction + chaseDist/speed < runnerTime
            // 即 reaction < runnerTime - chaseDist/speed
            double criticalReaction = runnerTime - (double) chaseDist / target.speed;

            if (criticalReaction <= minReact) {
                probMap.put(i, 1.0); // 反应再快也追不上
            } else if (criticalReaction >= maxReact) {
                probMap.put(i, 0.0); // 反应再慢也能追上
            } else {
                double prob = (criticalReaction - minReact) / (maxReact - minReact);
                probMap.put(i, Math.max(0, Math.min(1, prob)));
            }
        }
        return probMap;
    }

    // ==================== 扩展:多轮博弈模拟 ====================

    /**
     * 模拟多轮丢手绢游戏,统计各玩家被抓住概率
     */
    static Map<Integer, Integer> simulateGames(CircleQueue circle, int rounds) {
        Random rand = new Random(42);
        Map<Integer, Integer> caughtCount = new HashMap<>();
        int n = circle.size;
        int runner = 0; // 初始持绢者

        for (int r = 0; r < rounds; r++) {
            // 贪心策略:选择成功概率最高的目标
            Map<Integer, Double> probs = computeSuccessProbability(
                circle, runner, 0.5, 2.0);

            int bestTarget = -1;
            double bestProb = -1;
            for (Map.Entry<Integer, Double> e : probs.entrySet()) {
                if (e.getValue() > bestProb) {
                    bestProb = e.getValue();
                    bestTarget = e.getKey();
                }
            }

            // 模拟实际结果
            if (bestTarget != -1) {
                double actualReact = 0.5 + rand.nextDouble() * 1.5;
                Player runnerP = circle.players[runner];
                Player targetP = circle.players[bestTarget];
                int emptySeat = (targetP.position + n / 2) % n;
                int runDist = circle.shortestDistance(runnerP.position, emptySeat);
                int chaseDist = circle.shortestDistance(targetP.position, emptySeat);

                double rTime = (double) runDist / runnerP.speed;
                double tTime = actualReact + (double) chaseDist / targetP.speed;

                if (rTime >= tTime) {
                    // 被抓住,持绢者失败
                    caughtCount.merge(runner, 1, Integer::sum);
                    runner = bestTarget; // 被丢者成为新的持绢者
                } else {
                    // 逃脱成功
                    runner = bestTarget; // 被丢者成为新的持绢者(传统规则)
                }
            }
        }
        return caughtCount;
    }

    // ==================== 主程序与演示 ====================

    public static void main(String[] args) {
        int n = 12; // 12人围圈
        CircleQueue circle = new CircleQueue(n);

        // 初始化玩家:速度越快、反应越快越强
        circle.setPlayer(0, new Player(0, "小明", 3.0, 0.8));
        circle.setPlayer(1, new Player(1, "小红", 2.5, 1.0));
        circle.setPlayer(2, new Player(2, "小刚", 3.2, 0.9));
        circle.setPlayer(3, new Player(3, "小丽", 2.8, 1.2));
        circle.setPlayer(4, new Player(4, "小强", 3.5, 0.7));
        circle.setPlayer(5, new Player(5, "小芳", 2.6, 1.1));
        circle.setPlayer(6, new Player(6, "小军", 3.1, 0.9));
        circle.setPlayer(7, new Player(7, "小敏", 2.9, 1.0));
        circle.setPlayer(8, new Player(8, "小伟", 3.3, 0.8));
        circle.setPlayer(9, new Player(9, "小静", 2.7, 1.3));
        circle.setPlayer(10, new Player(10, "小波", 3.0, 1.0));
        circle.setPlayer(11, new Player(11, "小艳", 2.8, 1.1));

        System.out.println("========== 丢手绢游戏 - 环形追及分析 ==========");
        System.out.println("玩家人数: " + n);
        System.out.println();

        int runner = 4; // 假设小强持绢
        System.out.println("持绢者: " + circle.players[runner].name);

        // 分析对每个目标的成功概率
        Map<Integer, Double> probs = computeSuccessProbability(circle, runner, 0.5, 2.0);
        System.out.println("\n对各目标的逃脱成功概率:");
        for (int i = 0; i < n; i++) {
            if (i == runner) continue;
            System.out.printf("  %s: %.2f%%%n", circle.players[i].name, probs.getOrDefault(i, 0.0) * 100);
        }

        // 寻找最优目标
        List<Integer> best = findBestTargets(circle, runner);
        System.out.println("\n最优丢绢目标: ");
        for (int id : best) {
            System.out.println("  " + circle.players[id].name);
        }

        // 多轮模拟
        System.out.println("\n========== 1000轮博弈模拟 ==========");
        Map<Integer, Integer> caught = simulateGames(circle, 1000);
        System.out.println("被抓住次数统计:");
        for (int i = 0; i < n; i++) {
            int count = caught.getOrDefault(i, 0);
            System.out.printf("  %s: %d 次 (%.1f%%)%n", circle.players[i].name, count, count / 10.0);
        }
    }
}

算法复杂度分析

操作 时间复杂度 说明
单目标追及判定 O(1) 常数计算
最优目标搜索 O(n) 遍历所有玩家
成功概率计算 O(n) 每个目标一次计算
多轮模拟 O(rounds × n) 每轮需比较所有目标概率

环形结构使距离计算具有对称性,shortestDistance通过取模运算在O(1)内完成,是高效处理环形问题的关键技巧。

扩展思考

  1. 非均匀分布:若玩家围圈间距不等,需将位置建模为带权图,最短路径改用Dijkstra算法。
  2. 联盟策略:多名玩家可临时结盟围堵持绢者,引入多人博弈的纳什均衡分析。
  3. 信息不完全:被丢者不知谁被丢绢,直到邻座提示,可建模为部分可观察马尔可夫决策过程(POMDP)。

丢手绢将环形队列、追及问题与概率决策融为一体,是理解离散环形结构动态规划的绝佳案例。