每日算法 — 使用java实现钓鱼:泊松过程与最优收线时机决策

引言

钓鱼是一项考验耐心与判断力的休闲活动。经验丰富的钓手能通过观察浮漂的细微抖动、水面的波纹变化,精准判断提竿的最佳时机。本文将钓鱼中的等待与决策过程抽象为算法问题,用 Java 实现一个智能钓鱼系统。核心运用泊松过程建模鱼群随机上钩事件,结合最优停止理论分析何时收线才能获得最大期望收益,并引入多臂老虎机框架处理多钓点选择策略。

核心算法设计

1. 泊松过程建模鱼群上钩

鱼群随机游经钓点的过程符合泊松过程的假设:在任意相等时间间隔内,鱼上钩的概率相同,且不同时间段的事件相互独立。

设单位时间内平均上钩次数为 $\lambda$(称为到达率),则在时间 $t$ 内上钩 $k$ 条鱼的概率为:

$$P(N(t) = k) = \frac{(\lambda t)^k e^{-\lambda t}}{k!}$$

相邻两次上钩之间的时间间隔服从指数分布:
$$f(t) = \lambda e^{-\lambda t}$$

在模拟中,每次上钩的等待时间通过逆变换采样生成:$T = -\frac{\ln(U)}{\lambda}$,其中 $U \sim \text{Uniform}(0,1)$。

2. 最优收线时机决策

钓鱼的核心决策是”何时收线”。收线太早可能错过正在接近的鱼,收线太晚则已上钩的鱼可能脱钩。将问题建模为带截止时间的收益最大化问题:

  • 每次抛竿后有一个最大等待时间 $T_{max}$
  • 鱼上钩后有一个脱钩概率随时间递增
  • 不同大小的鱼有不同的价值权重

最优策略分析:
1. 阈值策略:当浮漂信号强度超过阈值 $\theta$ 时收线
2. 期望收益计算:$E[R] = \sum_{i} P(\text{鱼}i) \cdot V_i \cdot (1 – P{escape}(t))$
3. 最优阈值选择:通过历史数据统计使累计期望收益最大化的 $\theta^*$

3. 多钓点探索与利用

当钓手面对多个未知鱼情的钓点时,面临探索-利用权衡(Exploration-Exploitation Dilemma)。采用 UCB1 (Upper Confidence Bound) 算法:

$$UCB_i = \bar{x}_i + \sqrt{\frac{2 \ln n}{n_i}}$$

其中 $\bar{x}_i$ 是钓点 $i$ 的平均收益,$n_i$ 是该钓点的尝试次数,$n$ 是总尝试次数。第一项鼓励利用高回报钓点,第二项鼓励探索尝试次数少的钓点。

Java 实现

import java.util.*;

/**
 * 钓鱼系统:泊松过程建模与最优收线时机决策
 */
public class FishingSimulator {

    private static final Random random = new Random(42);

    // 鱼种定义
    enum FishType {
        SMALL("小鱼", 1.0, 0.3, 0.8),
        MEDIUM("中鱼", 3.0, 0.5, 0.5),
        LARGE("大鱼", 8.0, 0.15, 0.3),
        TROPHY(" trophy鱼", 20.0, 0.05, 0.15);

        final String name;
        final double value;      // 价值分数
        final double biteRate;   // 相对咬钩率
        final double catchRate;  // 基础捕获率

        FishType(String name, double value, double biteRate, double catchRate) {
            this.name = name;
            this.value = value;
            this.biteRate = biteRate;
            this.catchRate = catchRate;
        }
    }

    /**
     * 单次咬钩事件
     */
    static class BiteEvent {
        double time;           // 发生时间(秒)
        FishType fishType;     // 鱼种
        double signalStrength; // 浮漂信号强度 (0.0-1.0)
        double escapeRate;     // 脱钩速率 (每秒)

        BiteEvent(double time, FishType fishType) {
            this.time = time;
            this.fishType = fishType;
            // 信号强度与鱼大小正相关,但有噪声
            this.signalStrength = Math.min(1.0,
                fishType.biteRate * 1.5 + random.nextGaussian() * 0.15);
            this.escapeRate = 0.1 + random.nextDouble() * 0.3;
        }
    }

    /**
     * 钓点配置
     */
    static class FishingSpot {
        String name;
        double lambda;         // 泊松过程到达率 (次/分钟)
        Map<FishType, Double> fishDistribution;  // 鱼种分布概率
        int totalCasts;        // 总抛竿次数
        double totalReward;    // 累计收益

        FishingSpot(String name, double lambda) {
            this.name = name;
            this.lambda = lambda;
            this.fishDistribution = new HashMap<>();
            this.totalCasts = 0;
            this.totalReward = 0;
        }

        void setDistribution(Map<FishType, Double> dist) {
            this.fishDistribution = dist;
        }

        double getAverageReward() {
            return totalCasts > 0 ? totalReward / totalCasts : 0;
        }
    }

    /**
     * 单次抛竿模拟
     */
    static class CastResult {
        boolean success;       // 是否成功捕获
        FishType caughtFish;   // 捕获的鱼
        double waitTime;       // 等待时间
        double reward;         // 收益

        CastResult(boolean success, FishType fish, double waitTime, double reward) {
            this.success = success;
            this.caughtFish = fish;
            this.waitTime = waitTime;
            this.reward = reward;
        }
    }

    /**
     * 使用泊松过程生成一次抛竿中的咬钩事件序列
     */
    private List<BiteEvent> generateBiteEvents(FishingSpot spot, double maxWaitTime) {
        List<BiteEvent> events = new ArrayList<>();
        double currentTime = 0;

        while (currentTime < maxWaitTime) {
            // 指数分布采样:下一次咬钩等待时间
            double wait = -Math.log(1 - random.nextDouble()) / (spot.lambda / 60.0);
            currentTime += wait;
            if (currentTime >= maxWaitTime) break;

            // 按分布随机选择鱼种
            FishType type = sampleFishType(spot.fishDistribution);
            events.add(new BiteEvent(currentTime, type));
        }

        return events;
    }

    /**
     * 按概率分布采样鱼种
     */
    private FishType sampleFishType(Map<FishType, Double> dist) {
        double r = random.nextDouble();
        double cumsum = 0;
        for (Map.Entry<FishType, Double> entry : dist.entrySet()) {
            cumsum += entry.getValue();
            if (r <= cumsum) return entry.getKey();
        }
        return FishType.SMALL;
    }

    /**
     * 贪心阈值策略:当信号强度超过阈值时立即收线
     */
    public CastResult castWithThreshold(FishingSpot spot, double threshold, double maxWait) {
        List<BiteEvent> events = generateBiteEvents(spot, maxWait);
        spot.totalCasts++;

        for (BiteEvent event : events) {
            if (event.signalStrength >= threshold) {
                // 计算从咬钩到收线期间的脱钩概率
                double reactionDelay = 0.5 + random.nextDouble() * 1.0;  // 反应延迟0.5-1.5秒
                double escapeProb = 1.0 - Math.exp(-event.escapeRate * reactionDelay);

                if (random.nextDouble() > escapeProb) {
                    // 成功捕获
                    spot.totalReward += event.fishType.value;
                    return new CastResult(true, event.fishType, event.time + reactionDelay, event.fishType.value);
                } else {
                    // 脱钩,继续等待
                    continue;
                }
            }
        }

        // 超时未收线或所有咬钩都脱钩
        return new CastResult(false, null, maxWait, 0);
    }

    /**
     * 最优阈值搜索:通过模拟找到期望收益最大的收线阈值
     */
    public double findOptimalThreshold(FishingSpot spot, int simulations) {
        double bestThreshold = 0.5;
        double bestReward = 0;

        for (double threshold = 0.1; threshold <= 0.9; threshold += 0.05) {
            double totalReward = 0;
            // 临时保存状态
            int savedCasts = spot.totalCasts;
            double savedReward = spot.totalReward;
            spot.totalCasts = 0;
            spot.totalReward = 0;

            for (int i = 0; i < simulations; i++) {
                CastResult result = castWithThreshold(spot, threshold, 300);
                totalReward += result.reward;
            }

            double avgReward = totalReward / simulations;
            if (avgReward > bestReward) {
                bestReward = avgReward;
                bestThreshold = threshold;
            }

            // 恢复状态
            spot.totalCasts = savedCasts;
            spot.totalReward = savedReward;
        }

        return bestThreshold;
    }

    /**
     * UCB1多钓点选择策略
     */
    public int selectSpotUCB(List<FishingSpot> spots, int totalCasts) {
        int bestSpot = 0;
        double bestUCB = -1;

        for (int i = 0; i < spots.size(); i++) {
            FishingSpot spot = spots.get(i);
            double avgReward = spot.getAverageReward();
            double exploration = Math.sqrt(2 * Math.log(Math.max(1, totalCasts)) / Math.max(1, spot.totalCasts));
            double ucb = avgReward + exploration;

            if (ucb > bestUCB) {
                bestUCB = ucb;
                bestSpot = i;
            }
        }

        return bestSpot;
    }

    /**
     * 运行多钓点UCB策略模拟
     */
    public void runUCBSimulation(List<FishingSpot> spots, int rounds) {
        // 初始轮:每个钓点各抛竿2次
        for (FishingSpot spot : spots) {
            for (int i = 0; i < 2; i++) {
                double threshold = findOptimalThreshold(spot, 50);
                castWithThreshold(spot, threshold, 300);
            }
        }

        // UCB轮
        for (int round = 0; round < rounds; round++) {
            int spotIdx = selectSpotUCB(spots, round + spots.size() * 2);
            FishingSpot spot = spots.get(spotIdx);
            double threshold = findOptimalThreshold(spot, 50);
            CastResult result = castWithThreshold(spot, threshold, 300);
        }
    }

    public static void main(String[] args) {
        FishingSimulator sim = new FishingSimulator();

        // 配置三个不同鱼情的钓点
        FishingSpot spotA = new FishingSpot("深水区", 0.8);
        spotA.setDistribution(new HashMap<>() {{
            put(FishType.SMALL, 0.2);
            put(FishType.MEDIUM, 0.4);
            put(FishType.LARGE, 0.3);
            put(FishType.TROPHY, 0.1);
        }});

        FishingSpot spotB = new FishingSpot("浅水湾", 1.5);
        spotB.setDistribution(new HashMap<>() {{
            put(FishType.SMALL, 0.6);
            put(FishType.MEDIUM, 0.3);
            put(FishType.LARGE, 0.08);
            put(FishType.TROPHY, 0.02);
        }});

        FishingSpot spotC = new FishingSpot("芦苇荡", 0.5);
        spotC.setDistribution(new HashMap<>() {{
            put(FishType.SMALL, 0.1);
            put(FishType.MEDIUM, 0.2);
            put(FishType.LARGE, 0.4);
            put(FishType.TROPHY, 0.3);
        }});

        List<FishingSpot> spots = Arrays.asList(spotA, spotB, spotC);

        System.out.println("=== 智能钓鱼系统 ===\n");

        // 为每个钓点寻找最优收线阈值
        System.out.println("1. 各钓点最优收线阈值搜索:");
        for (FishingSpot spot : spots) {
            double optimal = sim.findOptimalThreshold(spot, 200);
            System.out.printf("   %s: 最优阈值=%.2f%n", spot.name, optimal);
        }

        // 运行UCB多钓点策略
        System.out.println("\n2. UCB多钓点策略模拟 (100轮):");
        sim.runUCBSimulation(spots, 100);
        for (FishingSpot spot : spots) {
            System.out.printf("   %s: 抛竿%d次, 平均收益=%.2f分%n",
                spot.name, spot.totalCasts, spot.getAverageReward());
        }

        // 单钓点详细模拟
        System.out.println("\n3. 深水区详细抛竿模拟 (10次):");
        double threshold = sim.findOptimalThreshold(spotA, 200);
        for (int i = 0; i < 10; i++) {
            CastResult r = sim.castWithThreshold(spotA, threshold, 300);
            System.out.printf("   第%d竿: %s, 等待%.1f秒, 收益%.1f分%n",
                i + 1,
                r.success ? "捕获" + r.caughtFish.name : "未中",
                r.waitTime, r.reward);
        }
    }
}

复杂度分析

模块 时间复杂度 空间复杂度 说明
泊松事件生成 $O(k)$ $O(k)$ $k$ 为单次抛竿的咬钩事件数,期望 $k = \lambda \cdot T_{max}$
阈值策略评估 $O(k)$ $O(1)$ 遍历事件列表,找到第一个超过阈值的事件
最优阈值搜索 $O(m \cdot n \cdot k)$ $O(1)$ $m$ 为阈值候选数,$n$ 为单次模拟次数
UCB钓点选择 $O(s)$ $O(1)$ $s$ 为钓点数量
UCB总模拟 $O(r \cdot s \cdot k)$ $O(s)$ $r$ 为总轮数

泊松过程的期望事件数 $k$ 通常很小(如 $\lambda=1$ 次/分钟,$T_{max}=5$ 分钟,则 $k \approx 5$),因此单次决策的计算开销极低,适合实时游戏场景。

总结

本文将钓鱼运动抽象为随机过程与最优决策的算法问题。泊松过程精确刻画了鱼群随机上钩的时间规律,阈值策略为每次咬钩提供了简洁高效的收线判断标准,UCB1 算法则解决了多钓点场景下的探索-利用权衡。该框架可进一步扩展:引入天气和时段对鱼情的影响、使用隐马尔可夫模型识别鱼群迁徙模式、或通过强化学习从历史垂钓数据中学习个性化的最优策略参数。