T407255 结点最短路径 题解

T407255 结点最短路径

题目提供者 难度 时间限制 内存限制 算法
动物联盟 普及/提高- 520ms 512.00MB 动态规划,dp搜索倍增动态规划,dp树形 dp最近公共祖先,LCA数论前缀和差分

解法

初步思想

对于任意一次询问,我们都可以使用 dijkstra 跑最短路。这棵树有 \(n-1\) 条边,那么单次 dijkstra 的时间复杂度就是 \(\mathcal O(n\log_2 n)\)\(m\) 个询问就是 \(\mathcal O(nm\log_2 n)\)。但是这题 \(1\le n,m\le 5\times 10^5\),因此这样做会死得很惨。我们要优化一下。

其实既然都是一棵树了,那么任意两个结点之间就只会有一条连接,而 dijkstra 跑的是最短路,用在上面显然是大材小用了。

我们可以这样想:对于树上面任意的两个点,都可以将两点之间的路径一分为二,转折的点就是他们两个点的最近公共祖先,因此我们就可以求最近公共祖先,然后让这两个点一步一步向最近公共跳过去,并记录答案。

最近公共祖先

倍增思想

\(dp_{(i,j)}\) 表示为 \(i\) 上面 \(2^j\) 层的父亲,那么就有递推式 \(dp_{(i,j)}=dp_{(dp_{(i,j-1)},j-1)}\)。根据这个递推式,我们就可以使用一个 \(\mathcal O(n\log_2 n)\) 的树上 dp 解决。接下来的思路当中,定义 \(d_x\) 表示 \(x\) 属于第几层的结点,\(f_x\) 表示为 \(x\) 的父亲。

当我们要求结点 \(x\) 和结点 \(y\) 的最近公共祖先时,如果 \(d_x\neq d_y\) 的话显然倍增算法不好跳,因此我们就需要将较深结点转移到较浅结点,使得 \(d_x=d_y\)。我们就假设 \(d_x>d_y\),如果不满足就交换 \(x\)\(y\)。然后当 \(d_x\) 仍然大于 \(d_y\) 的时候,\(x\) 就等于 \(dp_{(x,\log_2(d_x-d_y)-1)}\),也就是将 \(d_x\) 一步一步提到 \(d_y\)。这个操作的时间复杂度是 \(\mathcal O(\log_2 n)\) 的。

然后接下来就需要利用倍增同时往上跳了。可以从 \(\log_2(d_x)-1\) 开始,逐步递减到 \(1\),每一次如果离答案又进了一丢丢就往上跳,最后返回就行了。

最后我们发现求完最近公共祖先后无法 \(\mathcal O(1)\) 算出距离,那么我们就可以用树上前缀和的方式。设 \(p_i\) 表示为 \(1\)\(i\) 路径长度之和,那么 \(x\)\(y\) 的路径就可以拆成两段,使用差分我们就可以得到计算答案的方法:\(p_x-p_l+p_y-p_l=p_x+p_y-2\times p_l\)

时间复杂度 \(\mathcal O(m\log_2 n)\)

Tarjan

使用并查集,复杂度为 \(\mathcal O((n+m)k)\),十分优秀。但是由于这题 \(\mathcal O(m\log_2 n)\) 可过,因此不多做讲解。

代码

这里只有关键部分。

void dfs(ll x, ll f) {
  d[x] = d[f] + 1, dp[x][0] = f;
  // 请读者自行补充完整
  for (auto i : e[x]) {
    if (i.first != f) {
      p[i.first] = p[x] + i.second;
      dfs(i.first, x);
    }
  }
}

ll LCA(ll x, ll y) {
  for (d[x] < d[y] && (x ^= y, y ^= x, x ^= y); d[x] > d[y]; x = dp[x][lg[d[x] - d[y]] - 1]) { }
  if (x == y) {
    return x;
  }
  // 请读者自行补充完整
  return dp[x][0];
}

for (ll x, y; m; m--) {
  cin >> x >> y;
  ll l = LCA(x, y);
  cout << p[x] + p[y] - 2 * p[l] << '\n';
}
posted @ 2023-12-16 17:02  haokee  阅读(24)  评论(0)    收藏  举报