[ABC325E] Our clients, please wait a moment 题解

[ABC325E] Our clients, please wait a moment

题目描述

一个国家里有 \(n\) 个城市。
你需要从 \(1\) 号城市旅行到 \(n\) 号城市。
你有坐车和坐火车两种通行方式,对于从城市 \(i\) 到城市 \(j\)

  • 坐车会花费 \(D_{i,j} \times A\) 分钟
  • 坐火车会花费 \(D_{i,j} \times B+C\) 分钟

你可以在不花费任何时间的情况下从坐车切换为坐火车,但不能从坐火车切换为坐车。

问从城市 \(1\) 到城市 \(n\) 最少需要几分钟?

数据范围

  • $ 2\ \leq\ N\ \leq\ 1000 $
  • $ 1\ \leq\ A,\ B,\ C\ \leq\ 10^6 $
  • $ D_{i,j}\ \leq\ 10^6 $
  • $ D_{i,i}\ =\ 0 $
  • $ D_{i,j}\ =\ D_{j,i}\ >\ 0 $ $ (i\ \neq\ j) $
  • 入力される数値はすべて整数。

解法

前缀芝士🧀

Dijkstra 算法,一种求单源最短路径的算法。在算法中,以边权为拓扑序,因为在正权图当中每多增加一条边到最短路径当中,那么边权一定会变大。所以我们总是使用最短的边权,然后转移到相邻的点。如果那一个点转移的更优,那么就需要进行松弛操作,更新最短路径。

由于每次必会取最小值,我们可以进行最优化剪枝——当同一个结点比之前的劣,那么就退出。我们可以发现,拓扑序保证了结点的距离会慢慢增加,所以我们只需要记录这个结点是否出现过就行了,当出现过就直接退出。注意这不是可行性剪枝。

可以使用优先队列来快速求出最小的长度。由于使用了从小到大不断上升的拓扑序,所以不能处理负边权的图,因为这样会使路径长度不断下降,而最优化剪枝也就失去了作用,因此一般会死循环或求出错误的答案。需要遍历 \(n\) 个点和 \(m\) 条边,而优先队列进队列最多 \(n\) 次,因此时间复杂度为 \(\mathcal O((n+m)\log_2 n)\)

void dijkstra(ll s) {
  fill(d + 1, d + n + 1, 1e9);  // 初始化距离数组
  fill(f + 1, f + n + 1, 0);  // 初始化标记数组
  for (q.push({s, 0}), d[s] = 0; q.size(); q.pop()) {  // 入队并标记。由于拓扑序是上升的,因此可以最后在pop
    auto t = q.top();  // 获取队头
    if (f[t.to]) {  // 如果遍历过了
      continue;  // 最优性剪枝
    }
    f[t.to] = 1;  // 打上标记
    for (auto i : e[t.to]) {  // 遍历零点
      if (d[t.to] + i.v < d[i.to]) {  // 如果距离更近
        q.push({i.to, d[i.to] = d[t.to] + i.v});  // 更新并入队
      }
    }
  }
}

思路

根据题目我们可以知道,这题就是求点 \(1\) 到点 \(n\) 的最短距离距离,也就是时间。单源最短路径算法有 Dijkstra 和 SPFA,但是 \(1\le m(n^2)\le 10^6\) 的巨大数据只能够使用时间复杂度为 \(\mathcal O((n+m)\log_2 n)\) 的 Dijkstra 了。因此,解题结束。

但是,你做着做着,欸,不对啊,这里不能从坐火车切换成坐车,那我们怎么办?如果没有这种设定,那么我们就相当于如虎添翼,直接建图然后一甩手怼到 Dijkstra 脸上,然后让他跑完得出答案。但是有了这个设定就很烦躁,就像一阵微风拂过你的脸颊但是又飞过来一根铁柱。

此时你一定非常想杀人,但是要冷静。我们可以像一种方案,首先从 \(1\) 开始,只做车,跑最短路。然后再从 \(n\) 开始,只做船,在跑一遍最短路。跑完后,枚举每一个点,就假设他为 \(i\)。我们求出 \(1\sim i\) 的最短距离,然后再转过头去求 \(i\sim n\) 的最短距离。这样子,两条最短路径就拼了起来,形成了一条完整的最短路径。可谓是妙天下之解法也。

具体实现

我们定义两种 Dijkstra 函数,分别传参 \(s\) 表示起始点位:

  • dijkstra1:假设每次的边权是 \(x\),那么路径增加的长度就是 \(x\times A\)
  • dijkstra2:假设每次的边权是 \(x\),那么路径增加的长度就是 \(x\times B+C\)。注意 \(i\)\(j\) 的边权应该对应的是 \(D_{(j,i)}\) 而不是 \(D_{(i,j)}\),因为这里跑的是是反向的最短路。

假如第 \(i\) 个点到第 \(j\) 个点的最短路是 \(d_{(i,j)}\),我们只需要最后求一个值 \(\min\limits_{i=1}^n d_{(1,i)}+d_{(n,i)}\),表示连接起来的最短路的和。至此,本题结束。

代码

#include <iostream>
#include <queue>

using namespace std;
using ll = long long;

const ll kMaxN = 1005;

struct Node {
  ll to, v;
  friend bool operator<(const Node &a, const Node &b) {
    return a.v > b.v;
  }
} t;

ll e[kMaxN][kMaxN], d[kMaxN][kMaxN], f[kMaxN], n, a, b, c, ans = 1e18;
priority_queue<Node> q;

void dijkstra1(ll s) {
  fill(d[s] + 1, d[s] + n + 1, 1e18);
  fill(f + 1, f + n + 1, 0);
  for (q.push({s, 0}), d[s][s] = 0; q.size(); q.pop()) {
    if (f[(t = q.top()).to]) {
      continue;
    }
    f[t.to] = 1;
    for (ll i = 1; i <= n; i++) {
      if (d[s][t.to] + e[t.to][i] * a < d[s][i]) {
        q.push({i, d[s][i] = d[s][t.to] + e[t.to][i] * a});
      }
    }
  }
}

void dijkstra2(ll s) {
  fill(d[s] + 1, d[s] + n + 1, 1e18);
  fill(f + 1, f + n + 1, 0);
  for (q.push({s, 0}), d[s][s] = 0; q.size(); q.pop()) {
    if (f[(t = q.top()).to]) {
      continue;
    }
    f[t.to] = 1;
    for (ll i = 1; i <= n; i++) {
      if (d[s][t.to] + e[i][t.to] * b + c < d[s][i]) {
        q.push({i, d[s][i] = d[s][t.to] + e[i][t.to] * b + c});
      }
    }
  }
}

int main() {
  cin >> n >> a >> b >> c;
  for (ll i = 1; i <= n; i++) {
    for (ll j = 1; j <= n; j++) {
      cin >> e[i][j];
    }
  }
  dijkstra1(1);
  dijkstra2(n);
  for (ll i = 1; i <= n; i++) {
    ans = min(ans, d[1][i] + d[n][i]);
  }
  cout << ans << '\n';
  return 0;
}
posted @ 2023-11-21 18:02  haokee  阅读(131)  评论(1)    收藏  举报