引言
钓鱼是一项看似简单却蕴含丰富随机过程理论的活动。鱼群的到来服从特定的随机规律,钓手需要在”继续等待”和”收竿换点”之间做出最优决策。本文将使用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为钓点数量。
总结
本文将钓鱼问题抽象为泊松过程与最优停止决策问题,通过动态规划求解了最优等待阈值,并给出了多钓点的贪心选择策略。核心思想是将随机事件建模为概率过程,再利用优化理论找到收益最大化的决策规则。这种方法可以推广到客户服务、设备维护等具有”等待-决策”特征的场景。