Codeforces #677D Vanya and Treasure

tutorial

题目大意

有一个 \(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 $ 则由基本不等式有

\[\frac{\cnt(i) + \cnt(i+1)}{2} \ge \sqrt{\cnt(i) \cnt(i+1) } \ge \sqrt{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 $ 则有

\[nm = \sum_{i=1}^{p} \cnt(i) > \sum_{j = 1}^{k} \frac{\cnt(i_j) + \cnt(i_j + 1)}{2} \ge k\sqrt{nm} \]

从而有 $ k < \sqrt{nm} $ 。

松弛操作的总次数不超过 \(2nm\sqrt{nm}\)

证明:首先,我们有

\[\cnt(i) \cnt(i+1) < mn \implies \min(\cnt(i), \cnt(i+1)) < \sqrt{nm} \]

并且

\[\cnt(i) \cnt(i+1) =\min(\cnt(i), \cnt(i+1)) \times \max(\cnt(i), \cnt(i+1)) \]

因此

\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)\) 的最短路程。

  1. 用一个长为 \(n\) 的布尔数组标记哪些行中有标号为 \(i\) 点。
  2. 将每一列中标号为 \(i+1\) 的点所在的行的行号存入一个列表(std::vector<int>)。
  3. 对于每个含有标号为 \(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 下的评论

总结

这道题目是两年前的老题,我花了两天时间写完这篇博客。现在写博客都只讲思路和分析复杂度,不贴代码。

我认为若能对复杂度比较敏感,那么写出的代码也不会差。

posted @ 2018-06-14 19:59  Pat  阅读(204)  评论(0编辑  收藏  举报