Codeforces #677D Vanya and Treasure
题目大意
有一个 \(n \times m\) 的网格,网格上任意两个格点的距离定义为它们的曼哈顿距离。每个格点都有一个标号,第 \(i\) 行第 \(j\) 列的点标号为 \(a_{ij}\)(\(1\le a_{ij} \le p\)) 。对于 \(1\) 到 \(p\) 之间的每个整数 \(i\),至少存在一个格点标号为 \(i\),并且只有一个点标号为 \(p\) 。现欲从点 \((1,1)\) 依次经过标号为 \(1,2,3\dots, p-1\) 的点,最后到达标号为 \(p\) 的点,试求最短路的长度。
数据范围
$ 1 \le n, m \le 300 \( \) 1 \le p \le nm $
分析
How to approach the problem?
这道题可以看作是求分层图上两点间的最短路,分层图形如
分层图是一类特殊的 DAG,所以可用动态规划来求解单源最短路问题,复杂度为 \(O(E) = O(n^2m^2)\),这个复杂度是无法接受的。
在介绍更优的算法之前,先谈一谈对网格图的两种认识。
对于曼哈顿网格上的最短路问题,我们有两种方式来看待网格图。一是将网格图看作我们所关心的点的完全图 \(K_c\) (\(c\) 代表我们所关心的点的数目)或 \(K_c\) 的某个子图,边权是两端点对应的格点的曼哈顿距离;二是将网格图看作「网格」,亦即一个 \(n\times m\) 个点的无向图,每个点至多有 \(4\) 个邻点,所有边长度都是 \(1\) 。
上述「分层图上的 DP 算法」是用第一种观点来看待网格图。
算法二 \(\DeclareMathOperator{\cnt}{cnt}\)
用 \(\cnt(x)\) 表示网格中编号为 \(x\) 的点的数目,用 \(V_x\) 表示编号为 \(x\) 的点的集合;即有 \(\cnt(x) = |V_x|\) 。
当 \(\cnt(i)\times \cnt(i+1) < nm\) 时,遍历 \(V_i\) 和 \(V_{i+1}\) 之间的边,进行松弛操作。
当 $ \cnt(i)\times \cnt(i+1) \ge nm$ 时,从 \(V_i\) 中的所有点开始在网格图上 BFS 。
在网格图上 BFS 一次的复杂度为 \(O(nm)\) 。下面证明
调用 BFS 的次数至多为 \(\sqrt{nm}\)
首先,我们有 $\sum_{i=1}^{p} \cnt(i) = nm $ 且 \(\cnt(i) > 0\) 。
若 $\cnt(i) \cnt(i+1) \ge nm $ 则由基本不等式有
假设有 \(k\) 组相邻的标号 \((i_1, i_1 +1 ) , \dots, (i_j, i_j+1) \dots, (i_k, i_k+1)\) 使得 $\cnt(i_j) \cnt(i_j + 1) \ge nm $ 则有
从而有 $ k < \sqrt{nm} $ 。
松弛操作的总次数不超过 \(2nm\sqrt{nm}\)
证明:首先,我们有
并且
因此
\begin{aligned}
\sum_i \cnt(i) \cnt(i+1) &= \sum_i \min(\cnt(i), \cnt(i+1)) \times \max(\cnt(i), \cnt(i+1)) \\
&< \sqrt{nm} \sum_i \max(\cnt(i), \cnt(i+1)) \\
&< \sqrt{nm} 2 nm
\end{aligned}
其中求和指标 \(i\) 取遍使得 $ \cnt(i) \cnt(i+1) < nm $ 的标号。
至此,我们证明了此算法的复杂度为 \(O(nm\sqrt{nm})\) 。
这个算法还有一个变体,可以达到同样的渐近复杂度。
当 \(\cnt(i)\le \sqrt{nm}\) 且 \(\cnt(i+1) \le \sqrt{nm}\) 时,遍历 \(V_i\) 和 \(V_{i+1}\) 之间的边,进行松弛操作;否则在网格图上进行 BFS 。
不难证明,松弛操作的总次数不超过 \(2nm\sqrt{nm}\),调用 BFS 的次数不超过 \(2\sqrt{nm}\) 。
算法三 \(\DeclareMathOperator{\dp}{dp}\)
假设我们已经算出到每个标号为 \(i\) 的点的最短路长度,现在要据此算出到每个标号为 \(i+1\) 的点的最短路长度。
用 \(\dp[i][j]\) 表示从起点按要求走到点 \((i,j)\) 的最短路程。
- 用一个长为 \(n\) 的布尔数组标记哪些行中有标号为 \(i\) 点。
- 将每一列中标号为 \(i+1\) 的点所在的行的行号存入一个列表(
std::vector<int>
)。 - 对于每个含有标号为 \(i\) 的点的行 \(y\),先从左到右遍历这一行,行至点 \((y,x)\) 时,对于第 \(x\) 列中每个标号为 \(i+1\) 的点 \(p\),我们能够知道从起点经过 \((y,1),(y,2),\dots, (y,x)\) 中的某个标号为 \(i\) 的点到达点 \(p\) 的最短路程,用这个值更新 \(dp[y][x]\) 。再从右到左进行这个过程
我们来分析此算法的复杂度。
在遍历行的过程中行经的格点总数不超过 \(nm\cdot m\) 。
在遍历某一列中标号为 \(i+1\) 的点的过程中每个标号为 \(i+1\) 的点最多被访问 \(n\) 次(即每一行中都有编号为 \(i\) 的点),这部分访问的总节点数不超过 \(n\cdot nm\)
小结
为简便计,我们也把标号为 \(i\) 的点称作 \(i\) 类点。
我认为此算法针对「用 \(i\) 类点计算 \(i+1\) 类点」这个问题所采用方法是算法二中「在网格图上 BFS」的一个变体。做一形象描述就是:横向行走时,沿着网格图边走;纵向行走时,走「曼哈顿边」。
图中红色节点表示 \(i\) 类点,红色节点表示 \(i+1\) 类点,直线表示网格边,曲线表示「曼哈顿边」。
算法四
算法四和算法三类似,它利用了网格的几何性质。
对于每个 \(i+1\) 类点 \(p\) 我们可以找出至多 \(2(n+m)\) 个 \(i\) 类点,使得点 \(p\) 的 \(\dp\) 值可只由这些点的 \(\dp\) 值得到。实际上,只需要考虑每一行中横向上距离 \(p\) 最近的两个点和每一列中纵向上距离 \(p\) 最近的两个点。
算法五
用二维树状数组加速 DP 转移。想法上是暴力的,我不感兴趣。详见 TimonKnigge 在 tutorial 下的评论。
总结
这道题目是两年前的老题,我花了两天时间写完这篇博客。现在写博客都只讲思路和分析复杂度,不贴代码。
我认为若能对复杂度比较敏感,那么写出的代码也不会差。