引言
钓鱼是一项考验耐心与判断力的休闲活动。经验丰富的钓手能通过观察浮漂的细微抖动、水面的波纹变化,精准判断提竿的最佳时机。本文将钓鱼中的等待与决策过程抽象为算法问题,用 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 算法则解决了多钓点场景下的探索-利用权衡。该框架可进一步扩展:引入天气和时段对鱼情的影响、使用隐马尔可夫模型识别鱼群迁徙模式、或通过强化学习从历史垂钓数据中学习个性化的最优策略参数。