GESP8 复习讲义:Dijkstra 与树上差分课堂版 · C++14

本节课路线: 为什么这样讲 Dijkstra 概念 P11966 上学 差分概念 树上点差分 P3128 Max Flow 总结

一、为什么这节课只讲两个模型?

2026-06-06 这节课的目标不是把 GESP8 所有内容重新过一遍,而是把两个高频“听过但容易写不稳”的模型讲透:

1
Dijkstra 单源最短路

对应题目:P11966 上学。重点是“多次查询同一学校”怎样转成一次最短路。

2
树上点差分

对应题目:P3128 Max Flow P。重点是“很多树上路径加一”怎样不用暴力走路径。

3
代码稳定性

每个模型都要讲清适用条件、手推过程、关键代码和易错点。

课堂验收标准:学生不是“听懂了”,而是能回答:题面里哪句话触发这个模型?暴力为什么会超时?核心数组的含义是什么?复杂度为什么能过?

二、知识点:什么是单源最短路?

最短路问题可以理解成:在一张带权图中,从一个起点出发,走到其他点的最小代价是多少。

问题类型问法常见算法
单源最短路从一个点到很多点Dijkstra / SPFA / Bellman-Ford
全源最短路任意两点之间Floyd
最小生成树连接所有点总代价最小Kruskal / Prim
不要混淆:“从学校到每个家最短”是最短路;“修路连接所有学校最省钱”才是最小生成树。

Dijkstra 的核心思想

Dijkstra 是一个贪心算法。它每次从还没确定的点里,取出当前距离起点最近的点,然后用这个点去更新邻居。

1
初始化

起点距离为 0,其他点距离为无穷大。

2
取最近点

用小根堆快速拿到当前 dist 最小的点。

3
松弛邻居

如果 dist[u] + w < dist[v],就更新 v 的距离。

4
重复

直到堆为空,所有可达点的最短距离确定。

什么叫“松弛”?

可以把当前记录的距离看成一根被拉长的橡皮筋。如果发现一条更短的路,就把距离“放松”到更短。

S u v dist[u] w 如果 dist[u] + w 更小,就更新 dist[v]
Dijkstra 使用条件:边权不能为负。因为它一旦确定某个点的最短路,就默认以后不会再被负边“拉回来”变短。

三、题目讲义:P11966 [GESP202503 八级] 上学

1. 题意压缩

C 城是一个无向带权图。学校在点 s,有 q 个同学,每个同学家在 h_i。要求每个同学从家到学校的最短时间。

题面信息含义
无向图从家到学校 = 从学校到家
边长 l_i,速度 1 米/秒最短时间就是最短路长度
q 最多 2 × 10^5不能每个同学重新跑一次最短路
边权为正可以使用 Dijkstra
建模结论:从学校 s 作为源点跑一次 Dijkstra,得到 dist[x]。每个查询 h_i 直接输出 dist[h_i]

2. 为什么不能暴力?

如果每个同学都跑一次 Dijkstra,复杂度大约是:

q × (n + m) log n

q, n, m 都是 2 × 10^5 时,这个数量级完全不能接受。

3. 官方样例手推

样例中学校是 3,查询是 5, 1, 4。点击按钮观察 Dijkstra 的距离变化。

1 2 3 4 5 3 2 1 3 2
初始化

从学校 3 出发,dist[3] = 0,其他点先设为无穷大。

12345
dist0
小根堆先放入 (0,3)

4. P11966 课堂判断

等待选择。

5. P11966 参考代码

易错点:
  1. 默认 priority_queue 是大根堆,Dijkstra 需要小根堆。
  2. 无向边要加两次:u → vv → u
  3. 距离用 long long,不要用 int 硬扛。
  4. 堆里会有旧状态,弹出后要判断是否已经不是最新距离。

四、知识点:什么是差分?

差分是前缀和的逆运算。它的用途是:当我们要对一段区间反复加值时,不直接改整段,而是只在边界做标记。

一维差分例子

长度为 6 的数组初始全 0,现在要把区间 [2,4] 全部加 3。

下标123456
差分 D0+300-30
还原 A033300
核心思想:区间加值只改两个位置:D[l] += kD[r+1] -= k。最后做一次前缀和还原。

树上差分也是同一个思想,只是“还原”不再是从左到右前缀和,而是从子节点向父节点做 DFS 汇总。

五、知识点:树上点差分

树上点差分解决的问题是:很多次操作,每次把树上一条路径经过的所有点都加 1,最后问每个点被加了多少次。

为什么需要 LCA?

树上任意两点之间路径唯一。路径 u → v 会从 u 往上走到 LCA(u,v),再往下走到 v。所以 LCA 是两边贡献汇合的位置。

1 2 3 4 5 6 7

点差分公式

对于路径 u → v 上所有点加 1,设 p = LCA(u,v)

diff[u]++
diff[v]++
diff[p]--
diff[parent[p]]--
为什么 LCA 只减一次? 因为是“点差分”,路径经过 LCA 本身,所以 LCA 应该保留 1 次贡献。再减 LCA 的父亲,是为了不让贡献继续传到路径外。

点差分 vs 边差分

类型目标公式
点差分路径上的点加一u++, v++, lca--, parent(lca)--
边差分路径上的边加一u++, v++, lca -= 2

六、题目讲义:P3128 [USACO15DEC] Max Flow P

1. 题意压缩

有一棵树,给出 K 条运输路径 (s,t)。每条路径经过的所有点都加 1。最后输出被经过次数最多的点的次数。

题面信息含义
N 个点,N-1 条边,连通这是一棵树
每次给一对端点 s,t一次树上路径加一
K 到 100000不能每条路径都暴力走
最后只问最大值可以先全部标记,最后一次 DFS 汇总
建模结论:每条路径用 LCA + 点差分 O(log N) 标记,所有路径处理完后,用一次 DFS 自底向上汇总,取最大值。

2. 小树手推一条路径

路径 4 → 7,LCA 是 1。点击按钮看差分数组如何变化。

1 2 3 4 5 6 7 路径 4 → 7, LCA = 1
准备标记

路径是 4-2-1-3-7。我们不沿路加,而是在几个关键点做差分。

diff[4]++
diff[7]++
diff[1]--
diff[parent[1]]--
4710
diff0000

3. P3128 课堂判断

等待选择。

4. P3128 参考代码

易错点:
  1. 把点差分公式写成边差分公式。
  2. LCA 深度对齐时跳错方向。
  3. 忘记最后 DFS 汇总,直接拿 diff 当答案。
  4. parent[root] 是 0,数组下标 0 要能安全访问。

七、一句话总结

P11966:多次询问到同一个学校,转成“从学校出发的一次 Dijkstra”。

P3128:很多树上路径都要加一,转成“LCA + 点差分 + DFS 汇总”。

课后按错因补题

课堂暴露的问题补题目标
Dijkstra 模板不熟P4779 / P133930 分钟内写出堆优化最短路
LCA 不熟P3379能解释倍增跳法
树上点差分公式混乱P3128 重写 / P3258能手推点差分公式