1. 力扣周赛179场第三题深度解析
上周刚结束的力扣第179场周赛第三题,又一次让不少选手在提交时遇到了意想不到的坑。这道编号为1377的题目,表面看是标准的图论遍历,实则暗藏多个需要特别注意的边界条件。作为参加过三十多场周赛的老手,我在第一次提交时也栽了跟头,经过反复调试才找到最优解。今天就来拆解这道题的解题思路和那些容易踩的坑。
这道题描述的是在一个由n个节点组成的树形网络中,从1号节点开始传播信息,每次传播需要1个单位时间。当某个节点首次收到信息时,它会立即尝试向所有相邻节点传播,但每条边有特定的成功概率。要求计算在恰好t秒时,某个目标节点接收到信息的概率。题目给出的数据范围是n≤100,这提示我们可以考虑时间复杂度稍高但更稳妥的解法。
2. 题目分析与建模思路
2.1 问题重述与关键点提取
题目给出:
- 一棵包含n个节点的树(无向无环图)
- 信息从节点1开始传播
- 每条边e有一个传播成功率p(e)
- 传播过程:
- 当节点u首次收到信息时,会立即尝试向所有相邻未收到信息的节点传播
- 每次传播尝试耗时1秒
- 对每条边(u,v),传播成功概率为p(u,v),失败则不会再次尝试
- 要求计算在恰好t秒时,目标节点target收到信息的概率
关键观察点:
- 树结构保证了两点间路径唯一
- 传播是广度优先的,但成功概率影响最终结果
- 必须严格满足时间t的条件
2.2 解题思路选择
常见的三种思路对比如下:
| 方法 | 时间复杂度 | 适用场景 | 本题适用性 |
|---|---|---|---|
| BFS+概率计算 | O(n) | 常规树遍历 | 最优选择 |
| DFS+记忆化 | O(n^2) | 复杂图结构 | 过度设计 |
| DP预处理 | O(n^2) | 多次查询 | 不必要 |
经过分析,本题采用改进的BFS最为合适。因为:
- 树结构天然适合BFS遍历
- 传播的时间顺序正好对应BFS的层级扩展
- 单次查询不需要预处理
3. 算法实现细节
3.1 数据结构准备
首先需要构建图的邻接表表示,同时记录每条边的成功概率。在Python中可以用defaultdict实现:
python复制from collections import defaultdict
def build_graph(edges, succProb):
graph = defaultdict(list)
for i, (u, v) in enumerate(edges):
graph[u].append((v, succProb[i]))
graph[v].append((u, succProb[i])) # 无向图需要双向添加
return graph
3.2 BFS概率传播算法
核心算法采用带概率传播的BFS实现:
python复制def frogPosition(n, edges, t, target):
if n == 1: # 边界情况处理
return 1.0 if t >=0 else 0.0
graph = build_graph(edges)
visited = [False] * (n + 1)
prob = [0.0] * (n + 1)
prob[1] = 1.0
visited[1] = True
q = [1]
for _ in range(t):
next_q = []
for u in q:
unvisited_neighbors = [v for v, p in graph[u] if not visited[v]]
cnt = len(unvisited_neighbors)
if cnt == 0: # 无处可传,概率保持
next_q.append(u)
continue
for v, p in graph[u]:
if not visited[v]:
prob[v] = prob[u] * (1.0 / cnt) * p
visited[v] = True
next_q.append(v)
prob[u] = 0.0 # 已传播出去,当前节点概率清零
q = next_q
return prob[target]
3.3 关键步骤解析
-
初始化阶段:
- 构建图的邻接表表示
- 初始化访问数组和概率数组
- 起点概率设为1
-
BFS主循环:
- 每轮循环代表1秒的传播时间
- 对当前层的每个节点,找出所有未访问的邻居
- 计算传播到每个邻居的概率
- 更新邻居的概率值
-
概率计算规则:
- 每个节点将当前概率均分给所有未访问的邻居
- 再乘以对应边的成功概率
- 已传播的节点概率清零
-
终止条件:
- 严格运行t轮BFS
- 最后返回目标节点的概率值
4. 边界情况与特殊处理
4.1 常见边界情况
在实际编码中,需要特别注意以下几种边界情况:
-
单节点树:
- n=1时,无论t是多少,只要target=1,概率都是1
- 需要单独处理
-
目标节点是起点:
- target=1时:
- 如果t=0,概率为1
- 如果t>0且节点1有子节点,概率变为0(因为必须传播出去)
- target=1时:
-
时间t大于树深度:
- 当t足够大时,概率可能:
- 保持最后到达的状态(如果已经无法继续传播)
- 或者变为0(如果还能继续传播但目标节点未被访问)
- 当t足够大时,概率可能:
4.2 代码优化点
经过多次提交测试,发现以下几个优化点能提高通过率:
-
提前终止:
- 当某一轮没有新节点被访问时,可以提前结束BFS
- 因为后续轮次不会再改变概率分布
-
概率精度处理:
- 使用浮点数比较时考虑精度误差
- 可以设置一个很小的epsilon(如1e-8)来判断概率是否为0
-
内存优化:
- 对于大n,可以用字典代替数组存储概率
- 只保留当前层和下一层的节点信息
优化后的核心循环部分:
python复制for _ in range(t):
if not q: # 提前终止
break
next_q = []
for u in q:
neighbors = [v for v, p in graph[u] if not visited[v]]
if not neighbors:
next_q.append(u)
continue
p_share = prob[u] / len(neighbors)
prob[u] = 0.0
for v, p in graph[u]:
if not visited[v]:
prob[v] += p_share * p
visited[v] = True
next_q.append(v)
q = next_q
5. 复杂度分析与算法选择
5.1 时间复杂度
- 建图阶段:O(E),E为边数,在树中E=n-1
- BFS阶段:O(t * n),最坏情况下每轮遍历所有节点
- 总体:O(t * n)
由于题目中n≤100,t≤50,这个复杂度完全可接受
5.2 空间复杂度
- 邻接表存储:O(n)
- 访问数组和概率数组:O(n)
- 队列存储:O(n)
- 总体:O(n)
5.3 为什么不用DFS?
虽然DFS也能解决问题,但有几点不利因素:
- 需要额外处理时间维度
- 难以保证严格按时间顺序计算概率
- 递归深度可能导致栈溢出(虽然本题n较小不会)
- 代码复杂度更高,容易出错
6. 常见错误与调试技巧
6.1 常见错误类型
根据比赛统计和讨论区反馈,选手常犯的错误包括:
-
时间处理错误:
- 没有严格控制在t时刻的概率
- 错误地累加了所有≤t时刻的概率
-
概率分配错误:
- 忘记除以子节点数量
- 漏乘边的成功概率
- 没有清零已传播节点的概率
-
边界条件遗漏:
- 未处理n=1的情况
- 对t=0的情况处理不当
- 目标节点不可达时未返回0
6.2 调试技巧
-
小样例测试法:
构造简单的测试用例,如:- n=3, edges=[[1,2],[1,3]], t=1, target=2
预期结果应为0.5(假设概率都是1)
- n=3, edges=[[1,2],[1,3]], t=1, target=2
-
打印中间状态:
在每轮BFS后打印概率数组,观察传播过程:python复制print(f"After round {_}: {prob}") -
可视化工具:
对于更复杂的树结构,可以先用工具可视化树结构,如:- 使用graphviz生成树形图
- 手动绘制传播路径
7. 同类题目扩展
掌握这道题后,可以尝试解决以下几道相似题目,巩固图论概率传播问题的解法:
-
1514. 概率最大的路径(Dijkstra算法的变种)
- 求两点间成功概率最大的路径
- 需要将传统的Dijkstra算法中的距离相加改为概率相乘
-
1376. 通知所有员工所需的时间(简单版传播问题)
- 计算信息从根节点传播到所有叶子节点的最长时间
- 不需要考虑概率,简化版问题
-
1334. 阈值距离内邻居最少的城市(传播范围问题)
- 统计每个节点在给定距离内能到达的节点数
- 类似BFS传播思想
-
1376. Time Needed to Inform All Employees(树形结构传播时间)
- 计算信息从根节点传播到所有节点的最长时间
- 类似本题但不涉及概率
8. 竞赛策略与时间管理
在周赛环境中,这类题目通常需要在15-20分钟内完成。建议采用以下策略:
-
快速理解题意(2分钟):
- 画出样例的树结构
- 标记传播路径和概率
-
选择算法(1分钟):
- 识别出是树形结构
- 确定使用BFS+概率传播
-
编写代码(10分钟):
- 先处理边界情况
- 实现标准BFS框架
- 加入概率计算逻辑
-
测试调试(5分钟):
- 测试样例用例
- 构造极端情况(如n=1, t=0等)
- 检查浮点数精度
-
提交前检查(1分钟):
- 确认所有边界条件已处理
- 检查变量名是否写错
- 确保返回值类型正确
9. 个人实战心得
在多次周赛实战中,我总结了以下几点经验:
-
概率传播问题的通用模板:
- 使用BFS按层传播
- 维护一个概率数组
- 每层更新概率分布
- 注意清零已传播节点的概率
-
浮点数比较的注意事项:
- 避免直接比较浮点数是否相等
- 使用epsilon进行容错比较
- 可以考虑使用分数形式避免精度问题
-
竞赛中的时间取舍:
- 先确保基本解法通过
- 有余力再优化
- 不要过早陷入边界条件的泥潭
-
调试技巧:
- 打印中间状态是最高效的调试方法
- 对于树问题,先画图再编码
- 从简单样例入手逐步扩展
这道题教会我们,看似简单的BFS应用,加入概率因素后就会变得复杂许多。关键在于清晰地建模传播过程,并正确处理概率的分配与累积。在竞赛中遇到类似问题时,建议先完全理解样例,再着手编写代码,这样可以避免很多不必要的错误。
