每日算法 — 使用java实现钓鱼:泊松过程建模与最优等待窗口决策

引言

钓鱼是一项看似简单却蕴含丰富随机过程理论的活动。鱼群的到来服从特定的随机规律,钓手需要在”继续等待”和”收竿换点”之间做出最优决策。本文将使用Java实现一个智能钓鱼决策系统,核心采用泊松过程(Poisson Process)建模鱼群到达规律,结合最优停止理论(Optimal Stopping Theory)和动态规划方法,计算最佳收竿时机与等待策略。

问题建模

假设在某一钓点,鱼群的咬钩事件服从参数为 λ 的泊松过程:
– 单位时间内咬钩次数的期望为 λ
– 两次咬钩之间的时间间隔服从指数分布 Exp(λ)
– 非重叠时间区间内的咬钩次数相互独立

钓手每次抛竿后面临决策:
1. 当前已等待时间 t,是否应该继续等待?
2. 如果换到新钓点,新钓点的 λ 可能不同,但换点有固定成本 C

泊松过程与参数估计

public class PoissonProcessModel {
    private double lambda; // 单位时间平均到达率
    private final List<Double> interArrivalTimes = new ArrayList<>();

    /**
     * 记录一次咬钩事件,更新到达率估计
     */
    public void recordBite(double currentTime) {
        if (!interArrivalTimes.isEmpty()) {
            double lastTime = getLastArrivalTime();
            interArrivalTimes.add(currentTime - lastTime);
        } else {
            interArrivalTimes.add(currentTime);
        }
        updateLambdaEstimate();
    }

    /**
     * 使用最大似然估计更新lambda参数
     * lambda_MLE = n / total_time
     */
    private void updateLambdaEstimate() {
        if (interArrivalTimes.size() >= 3) {
            double totalTime = interArrivalTimes.stream().mapToDouble(Double::doubleValue).sum();
            lambda = interArrivalTimes.size() / totalTime;
        }
    }

    /**
     * 指数分布采样:模拟下一次咬钩等待时间
     */
    public double sampleNextInterArrival(Random random) {
        // 逆变换采样:X = -ln(U) / lambda
        return -Math.log(1.0 - random.nextDouble()) / lambda;
    }

    /**
     * 在时间窗口T内咬钩次数的期望
     */
    public double expectedBites(double timeWindow) {
        return lambda * timeWindow;
    }

    /**
     * 在时间T内至少咬钩一次的概率
     */
    public double probabilityOfAtLeastOneBite(double timeWindow) {
        return 1.0 - Math.exp(-lambda * timeWindow);
    }

    public double getLambda() {
        return lambda;
    }

    private double getLastArrivalTime() {
        return interArrivalTimes.stream().mapToDouble(Double::doubleValue).sum();
    }
}

最优停止理论:最佳收竿时机

public class OptimalStoppingSolver {
    private static final double TIME_STEP = 0.1; // 离散化时间步长(分钟)
    private static final double MAX_WAIT = 30.0; // 最大等待时间(分钟)

    /**
     * 使用动态规划求解最优停止策略
     * 状态:已等待时间 t
     * 决策:继续等待 或 收竿换点
     * 
     * V(t) = max{ 收竿期望收益, 继续等待的期望收益 }
     * 
     * 继续等待期望 = integral_0^inf [ P(在dt内咬钩) * (咬钩收益 + V(0)) 
     *                               + P(未咬钩) * V(t+dt) ]
     */
    public double[] solveOptimalPolicy(double lambda, double biteValue, 
                                        double switchingCost, double timeDiscount) {
        int nSteps = (int) (MAX_WAIT / TIME_STEP) + 1;
        double[] V = new double[nSteps]; // V[i] = 已等待 i*TIME_STEP 时的最优期望收益
        boolean[] shouldSwitch = new boolean[nSteps];

        // 逆向归纳:从最大等待时间开始
        for (int i = nSteps - 1; i >= 0; i--) {
            double t = i * TIME_STEP;

            // 选项1:立即收竿换点
            double switchValue = -switchingCost + V[0] * Math.exp(-timeDiscount * TIME_STEP);

            // 选项2:继续等待一个时间步
            double pBite = lambda * TIME_STEP; // 在dt内咬钩概率(近似)
            pBite = Math.min(pBite, 1.0);

            double waitValue;
            if (i == nSteps - 1) {
                waitValue = pBite * biteValue; // 边界:必须做出选择
            } else {
                waitValue = pBite * (biteValue + V[0] * Math.exp(-timeDiscount * TIME_STEP))
                          + (1 - pBite) * V[i + 1] * Math.exp(-timeDiscount * TIME_STEP);
            }

            if (switchValue > waitValue) {
                V[i] = switchValue;
                shouldSwitch[i] = true;
            } else {
                V[i] = waitValue;
                shouldSwitch[i] = false;
            }
        }

        return V;
    }

    /**
     * 获取最优切换阈值:首次应该收竿换点的时间
     */
    public double getSwitchingThreshold(double lambda, double biteValue,
                                         double switchingCost, double timeDiscount) {
        double[] V = solveOptimalPolicy(lambda, biteValue, switchingCost, timeDiscount);
        int nSteps = V.length;

        for (int i = 0; i < nSteps; i++) {
            double t = i * TIME_STEP;
            // 检查在时间t继续等待 vs 换点的收益差异
            double pBite = Math.min(lambda * TIME_STEP, 1.0);
            double switchValue = -switchingCost + V[0] * Math.exp(-timeDiscount * TIME_STEP);
            double waitValue;
            if (i == nSteps - 1) {
                waitValue = pBite * biteValue;
            } else {
                waitValue = pBite * (biteValue + V[0] * Math.exp(-timeDiscount * TIME_STEP))
                          + (1 - pBite) * V[i + 1] * Math.exp(-timeDiscount * TIME_STEP);
            }

            if (switchValue >= waitValue) {
                return t;
            }
        }
        return MAX_WAIT;
    }
}

多钓点动态决策

public class MultiSpotFishingOptimizer {

    static class FishingSpot {
        String name;
        double lambda; // 到达率
        double avgFishWeight; // 平均鱼重
        double travelTime; // 到达该点所需时间

        FishingSpot(String name, double lambda, double weight, double travel) {
            this.name = name; this.lambda = lambda;
            this.avgFishWeight = weight; this.travelTime = travel;
        }
    }

    /**
     * 贪心策略:始终选择当前期望收益最高的钓点
     * 期望收益 = lambda * avgWeight * 可用时间 - travelCost
     */
    public FishingSpot selectBestSpotGreedy(List<FishingSpot> spots, 
                                            double remainingTime, double timeValue) {
        FishingSpot best = null;
        double bestRate = -1;

        for (FishingSpot spot : spots) {
            double effectiveTime = Math.max(0, remainingTime - spot.travelTime);
            double expectedCatch = spot.lambda * effectiveTime; // 期望捕获次数
            double expectedWeight = expectedCatch * spot.avgFishWeight;
            double netValue = expectedWeight - spot.travelTime * timeValue;

            if (netValue > bestRate) {
                bestRate = netValue;
                best = spot;
            }
        }
        return best;
    }

    /**
     * 动态规划求解最优钓点序列
     * 状态:剩余时间,已访问钓点集合
     */
    public List<FishingSpot> solveOptimalRoute(List<FishingSpot> spots, double totalTime) {
        int n = spots.size();
        // 简化为按单位时间收益排序的贪心策略
        return spots.stream()
            .sorted((a, b) -> {
                double rateA = a.lambda * a.avgFishWeight;
                double rateB = b.lambda * b.avgFishWeight;
                return Double.compare(rateB, rateA);
            })
            .collect(java.util.stream.Collectors.toList());
    }
}

完整模拟系统

public class FishingSimulator {
    public static void main(String[] args) {
        Random random = new Random(42);

        // 初始化钓点参数
        PoissonProcessModel model = new PoissonProcessModel();
        // 假设历史数据表明lambda约为0.2(每5分钟一次咬钩)
        for (int i = 0; i < 10; i++) {
            model.recordBite((i + 1) * 5.0 + random.nextGaussian() * 2);
        }

        double lambda = model.getLambda();
        System.out.printf("估计的到达率 lambda = %.4f (次/分钟)%n", lambda);
        System.out.printf("期望等待时间 = %.2f 分钟%n", 1.0 / lambda);

        // 求解最优策略
        double biteValue = 10.0; // 每次咬钩的价值
        double switchingCost = 5.0; // 换点成本
        double timeDiscount = 0.01; // 时间折现率

        OptimalStoppingSolver solver = new OptimalStoppingSolver();
        double threshold = solver.getSwitchingThreshold(lambda, biteValue, switchingCost, timeDiscount);
        System.out.printf("\n最优切换阈值: %.2f 分钟%n", threshold);
        System.out.printf("建议: 若等待超过 %.1f 分钟未咬钩,应收竿换点%n", threshold);

        // 蒙特卡洛验证
        System.out.println("\n=== 蒙特卡洛模拟验证 ===");
        int nSims = 10000;
        double totalGreedy = 0, totalPatient = 0;

        for (int sim = 0; sim < nSims; sim++) {
            totalGreedy += simulateStrategy(random, lambda, threshold, true);
            totalPatient += simulateStrategy(random, lambda, threshold, false);
        }

        System.out.printf("贪心策略(阈值%.1f分钟)平均收益: %.2f%n", threshold, totalGreedy / nSims);
        System.out.printf("耐心策略(始终等待)平均收益: %.2f%n", totalPatient / nSims);
    }

    private static double simulateStrategy(Random random, double lambda, 
                                           double threshold, boolean useThreshold) {
        double totalValue = 0;
        double totalTime = 0;
        double sessionDuration = 120; // 2小时钓鱼时间

        while (totalTime < sessionDuration) {
            double waitTime = 0;
            boolean caught = false;

            while (waitTime < sessionDuration - totalTime) {
                // 模拟下一个时间步
                double dt = 0.5;
                double pBite = 1.0 - Math.exp(-lambda * dt);

                if (random.nextDouble() < pBite) {
                    totalValue += 10.0;
                    totalTime += waitTime + dt;
                    caught = true;
                    break;
                }

                waitTime += dt;
                if (useThreshold && waitTime >= threshold) {
                    totalTime += waitTime + 2.0; // 换点耗时2分钟
                    break;
                }
            }

            if (!caught && waitTime >= sessionDuration - totalTime) break;
        }

        return totalValue;
    }
}

复杂度分析

模块 时间复杂度 空间复杂度
参数估计(MLE) O(n) O(n)
最优策略DP求解 O(T/Δt) O(T/Δt)
蒙特卡洛模拟 O(SIM_COUNT × session/Δt) O(1)
多钓点贪心选择 O(m) O(1)

其中n为历史观测次数,T为最大等待时间,Δt为离散化步长,m为钓点数量。

总结

本文将钓鱼问题抽象为泊松过程与最优停止决策问题,通过动态规划求解了最优等待阈值,并给出了多钓点的贪心选择策略。核心思想是将随机事件建模为概率过程,再利用优化理论找到收益最大化的决策规则。这种方法可以推广到客户服务、设备维护等具有”等待-决策”特征的场景。