P5025 [SNOI2017] 炸弹 解题报告
P5025 [SNOI2017] 炸弹 解题报告
1. 题目解读:这道题到底要我们干什么?
想象一条很长的线上放着 n
个炸弹。每个炸弹 i
都有一个坐标 x_i
和一个爆炸半径 r_i
。
当一个炸弹(比如第 i
个)爆炸时,它会形成一个冲击波。任何处于 [x_i - r_i, x_i + r_i]
这个区间内的其他炸弹(比如第 j
个)都会被引爆。这种引爆是连锁的,j
爆炸后可能又会引爆其他炸弹。
我们的任务是: 对于每一个炸弹 i
(从 1 到 n
),计算如果手动引爆它,总共会引爆多少个炸弹(包括它自己)。最后,将每个炸弹 i
的引爆数量乘以它的编号 i
,再把这些结果全部加起来,得到一个最终的总和。
2. 核心思路:从暴力到优化的思考过程
第一步:最直观的想法 —— 建模为图
这是一个典型的“连锁反应”问题,很自然地可以联想到图论。
- 点(Vertex):每个炸弹看作一个图中的点。
- 边(Edge):如果炸弹
i
的爆炸范围能覆盖到炸弹j
,我们就从i
向j
连一条有向边i -> j
。这表示i
的爆炸可以直接导致j
的爆炸。
这样,问题就转化成了:对于图中的每一个点 i
,找出从它出发总共能到达多少个不同的点(即可达节点的数量)。
第二步:朴素解法及其瓶颈
根据上面的图模型,我们可以这样做:
- 建图:对于每个炸弹
i
,遍历所有其他炸弹j
。如果满足|x_j - x_i| <= r_i
,就添加一条边i -> j
。 - 统计:对于每个炸弹
i
,从点i
开始进行一次深度优先搜索(DFS)或广度优先搜索(BFS),统计所有能访问到的点的数量。
问题出在哪里?
数据范围 n
高达 500,000。
- 建图:需要两层循环,时间复杂度是 O(n²),大约是
500000 * 500000
,这太慢了。 - 统计:
n
次搜索,每次的复杂度也可能很高。
所以,这个 O(n²) 的建图过程是主要瓶颈,必须优化。
第三步:优化的曙光 —— 区间连边
我们观察到,题面中有一个重要条件:x_i
是严格递增的。这意味着炸弹是按坐标排好序的。
因此,当炸弹 i
爆炸时,它能引爆的炸弹 j
的坐标范围是 [x_i - r_i, x_i + r_i]
。由于所有炸弹的坐标 x
是有序的,那么这些被引爆的炸弹的编号也必然在一个连续的区间 [L, R]
内。
- 我们可以用二分查找(
lower_bound
和upper_bound
)快速地找到这个编号区间[L, R]
。
现在,建图的问题从“点 i
向一堆零散的点 j
连边”变成了“点 i
向一整个区间 [L, R]
内的所有点连边”。
虽然问题形式变了,但如果还是老实地把 i
和 L, L+1, ..., R
里的每个点都连一条边,复杂度依然是 O(n²)。我们需要一种更高效的方式来处理“点向区间连边”。
第四步:神器登场 —— 线段树优化建图
这就是线段树大显身手的地方了。我们可以把点向区间的连边,变成点向少数几个代表这个区间的“大节点”连边。
-
建立一棵线段树:这棵线段树的叶子节点代表每个具体的炸弹(编号 1 到
n
)。每个非叶子节点则代表一个编号区间。为了保证图的连通性,我们还需要从每个父节点向它的两个子节点连边。这代表着,如果一个炸弹能引爆区间[l, r]
,它自然也能引爆其中的子区间[l, mid]
和[mid+1, r]
。
(上图很好地展示了这个思想,最下面一排是原始的点,上面是代表区间的节点) -
执行连边:当我们需要从炸弹
i
向区间[L, R]
连边时,我们在线段树上找到能完整覆盖[L, R]
的log(n)
级别的节点,然后从i
对应的叶子节点向这些“区间代表节点”连边。
通过这种方式,我们把总的边数从 O(n²) 级别成功地降低到了 O(n log n) 级别,时空复杂度都得到了极大的改善。
3. 解决深层问题:相互引爆与强连通分量
建好了图,我们还需要解决一个问题:如果炸弹 A 能引爆 B,同时 B 也能引爆 A,它们就形成了一个“小团体”。任何能引爆 A 的炸弹,也就能引爆这个团体里的所有成员。
这种“小团体”在图论中被称为强连通分量(Strongly Connected Component, SCC)。在同一个 SCC 里的所有点都是相互可达的。
- Tarjan 算法与缩点:我们可以使用经典的 Tarjan 算法来找出图中所有的 SCC。然后,我们将每个 SCC “压缩”成一个新的超级点。这个过程叫做缩点。
- 构建新图(DAG):缩点之后,原来的复杂图就变成了一个更简单的有向无环图(DAG)。新图中的边,就是原来两个不同 SCC 之间的连接。每个超级点的“权值”就是它内部包含的原始炸弹的数量。
4. 在新图上计算最终答案
现在问题变得清晰了:在一个 DAG 上,对于每个(包含原始炸弹 i
的)超级点,计算它能到达的所有超级点的总权值之和。
- 在 DAG 上进行 DFS:由于是 DAG,我们可以通过一次 DFS 来更新每个超级点能到达的“势力范围”。在题解代码中,这是通过维护每个超级点最终能引爆的炸弹编号的最小和最大值
left[scc]
和right[scc]
来实现的。 - 更新范围:对于一个超级点
u
,它最终能到达的范围,是它自己本身的范围,以及它能直接到达的所有下游超级点v
的范围的并集。我们在 DAG 上进行一次反向拓扑序的更新(或者说,一次 DFS 遍历)即可完成这个计算。 - 统计答案:经过最后的 DFS,我们得到了每个超级点最终能引爆的炸弹编号区间
[left, right]
。对于原始的每个炸弹i
,它所在的超级点能引爆的炸弹总数就是right - left + 1
。 - 计算总和:最后,遍历炸弹 1 到
n
,将i * (引爆数量)
累加到最终答案ans
中,并按要求取模。
总结
本题的解法是一个组合拳,环环相扣:
- 图论建模:将问题转化为图的连通性问题。
- 线段树优化建图:利用坐标有序的特性,将 O(n²) 的点对点连边优化为 O(n log n) 的点对区间连边,解决了核心瓶颈。
- Tarjan 缩点:处理图中复杂的环(相互引爆),将图简化为 DAG。
- DAG 上的 DP/DFS:在新图上高效地统计每个点最终的“战果”。
通过这一系列步骤,我们将一个看似棘手的难题分解成了几个经典的算法模块,最终在要求的时间复杂度内解决了问题。