最短路反向建边问题

本文探讨了一道涉及多头牛和单向道路的最短路问题,介绍了如何通过反向建边和双源最短路径算法求解从牛原地到农场再返回的最长时间。策略包括单起点多终点、多个起点单终点的转换和优化方法。

在今天的限时训练中遇到了一道有意思的最短路问题,现在来跟大家分享一下。

题意:

One cow from each of N farms (1 ≤ N ≤ 1000) conveniently numbered 1..N is going to attend the big cow party to be held at farm #X (1 ≤ X ≤ N). A total of M (1 ≤ M ≤ 100,000) unidirectional (one-way roads connects pairs of farms; road i requires Ti (1 ≤ Ti ≤ 100) units of time to traverse.

Each cow must walk to the party and, when the party is over, return to her farm. Each cow is lazy and thus picks an optimal route with the shortest time. A cow's return route might be different from her original route to the party since roads are one-way.

Of all the cows, what is the longest amount of time a cow must spend walking to the party and back?

Input

Line 1: Three space-separated integers, respectively: 

<think> 我们参考上述引用内容,特别是关于分层图短路在洛谷P4568题中的应用。 题目大意:有n个城市,m条航线,每条航线连接两个城市,有花费。现在有k次免费乘坐航线的机会,求从起点s到终点t的小花费。 分层图短路的思路: 1. 构k+1层图(0到k层),每一层都是原图的复制。 2. 对于原图中的一条(u, v, w),我们在每一层内部都立双向(即同层之间正常连权为w)。 3. 在层与层之间,我们立单向(从当前层指向下一层),权为0,表示使用一次免费机会。具体来说,对于每一层i(0<=i<k),从第i层的u节点向第i+1层的v节点连一条权值为0的,同时从第i层的v节点向第i+1层的u节点连一条权值为0的(因为是无向图)。 4. 然后我们以第0层的起点为源点,求单源短路(可以使用Dijkstra算法)。 5. 后,我们检查每一层的终点(即第0层到第k层的终点)的短路值,取小值作为答案。 注意:由于有k+1层,所以节点编号需要重新映射。通常,第i层的节点j的编号可以表示为:j + i * n(其中n是原图节点数,节点编号从0或1开始,题目中节点编号从0开始,共n个节点)。 举例说明: 原图有n个节点,那么第0层节点编号为0~n-1,第1层节点编号为n~2n-1,...,第k层节点编号为k*n ~ (k+1)*n-1。 图步骤: 1. 首先,读入n, m, k, s, t(注意题目中节点编号从0开始)。 2. 对于每条(u, v, w): a. 在每一层内部(0~k层)都立双向,即同层内u和v之间权为w。 但是,我们不需要显式地k+1层,而是通过节点编号的映射,在每一层内部都立相同的。 具体操作:对于每一层i(0<=i<=k),在节点(u+i*n)和(v+i*n)之间立双向权为w。 但实际上,我们可以在读入每条后,循环i从0到k,分别添加:从u+i*n到v+i*n的权为w的双向。 b. 然后,在层与层之间,对于i从0到k-1,立从u+i*n到v+(i+1)*n的单向权0),以及从v+i*n到u+(i+1)*n的单向权0)。 这表示在节点u(第i层)使用一次免费机会,可以到达节点v(第i+1层);同理,在节点v(第i层)使用一次免费机会,可以到达节点u(第i+1层)。 3. 运行短路算法(如Dijkstra),起点为s(第0层起点,即s+0*n = s)。 4. 遍历每一层的终点t,即dis[t + i * n](i从0到k),取小值。 但是,我们也可以不显式地k+1层图,而是采用动态规划的思想,在状态中增加一维表示使用了几次免费机会。不过,分层图图是直观的。 另外,注意空间:节点总数为n*(k+1),数:每一层有m条(双向,相当于2m条有向),层与层之间有2m条有向(每层之间只有一层有向,共k层),所以总数=(k+1)*2m + k*2m = (2k+2+2k)*m/2? 实际为: 同层:每一层有m条双向,即每层2m条有向,共k+1层,所以同层总数为 (k+1)*2m。 层间:每一条原会在层间产生2条有向(从当前层的u到下一层的v,以及当前层的v到下一层的u),而层间共有k层(从0到k-1),所以层间总数为 k*2m。 因此总数 = (k+1)*2m + k*2m = (4k+2)*m? 注意双向我们了两次(同层内双向两条有向,层间也是两个方向分别一次,但是层间是单向的,所以层间只有两个方向各一条?其实层间是单向的,我们了两条:u->v'和v->u',所以是2条)。实际上,同层:每读入一条(u,v,w),我们在每一层都两条有向:u+i*n -> v+i*n (w) 和 v+i*n -> u+i*n (w),共k+1层,所以2*(k+1)条有向。 层间:同样,每读入一条(u,v,w),对于每一层i(0<=i<k),我们两条有向:u+i*n -> v+(i+1)*n (0) 和 v+i*n -> u+(i+1)*n (0),共k层,所以2*k条有向。 总数:每条原对应 (2*(k+1) + 2*k) = 4k+2 条有向。 当k较大时,数会很大,所以要注意数据范围。本题中,k<=10,n<=10000,m<=50000,所以总大为:50000*(4*10+2)=50000*42=2100000,在可接受范围内。 代码步骤(以Dijkstra为例): 1. 初始化dis数组,大小为n*(k+1),初始化为一个很大的数。dis[起点] = 0,起点为s(即s+0*n)。 2. 优先队列(小根堆),将(0, s)入队(注意:这里s表示第0层的起点)。 3. 当队列非空,取出堆顶元素,设当前节点u,当前距离d。 - 如果d不等于dis[u],跳过。 - 否则,遍历u的所有出,进行松弛操作。 4. 终,遍历所有层(0到k)的终点t,取小值。 注意:节点编号的转换:原图中的节点编号x,在第i层的编号为x+i*n。 下面给出一个伪代码(实际代码需要根据具体输入调整): 假设节点编号从0开始。 伪代码: n, m, k, s, t = 输入 // 总节点数 total_nodes = n * (k+1); // 图,用邻接表 vector<vector<pair<int, int>>> graph(total_nodes); // 邻接表,pair<邻居, 权> // 读入m条 for i=0 to m-1: u, v, w = 输入 for layer in 0 to k: // 同层内双向 from1 = u + layer * n; to1 = v + layer * n; graph[from1].push_back({to1, w}); graph[to1].push_back({from1, w}); if layer < k: // 当前层到下一层,单向(两条,因为无向图) from2 = u + layer * n; to2 = v + (layer+1)*n; graph[from2].push_back({to2, 0}); from3 = v + layer * n; to3 = u + (layer+1)*n; graph[from3].push_back({to3, 0}); // 注意:上面的层间了两次(两个方向),但实际上题目是无向图,所以这样是正确的。 // 但是,这里有一个问题:同层内双向时,每一层都了双向,而层间则只了从当前层到下一层的单向(且两个方向都了)。 // 运行Dijkstra dis = [inf] * total_nodes dis[s] = 0; // 第0层的起点s priority_queue<pair<int, int>> pq; // (距离, 节点) 小根堆,距离取负或者用greater pq.push({0, s}); while not pq.empty(): (d, u) = pq.top(); pq.pop(); if d != dis[u]: continue; for each edge in graph[u]: v = edge.first, w = edge.second; if dis[u] + w < dis[v]: dis[v] = dis[u] + w; pq.push({dis[v], v}); // 遍历所有层的终点 ans = inf; for i in 0 to k: ans = min(ans, dis[t + i*n]); 输出 ans; 但是,注意:上面的图方式可能会导致层内重复?实际上,对于每条,我们在每一层内都了双向,同时对于层间,我们在相邻两层之间了单向(表示免费)。这样图是符合题意的。 然而,上述图在层内部分,每个无向在每一层内都被拆成了两条有向,而层间部分,每个无向在相邻两层之间也被拆成了两条有向(但是是单向的,从当前层指向下一层)。 但是,引用[3]提到:层间会存在从上一层的点1到下一层的点0、点2的0权。这里只了单向(从上层到下层),所以我们的图符合。 但是,我们注意到,层间是单向的,只能从当前层到下一层,不能从下一层回到上一层,这符合免费机会只能使用,不能退回。 另外,我们也可以不同层内的双向,而是只原图的双向,然后层间双向?不对,层间是单向的,因为我们只能使用免费机会从当前层到下一层。 因此,上述图方式是正确的。 但是,还有一种常见的图方式:只原图(第0层)的,然后在层间时,不仅0权,同时也保留原到下一层?不对,实际上,我们有两种选择:使用免费或者不使用免费。 所以,对于同一条,我们在同一层内(表示不使用免费),然后在当前层和下一层之间0权(表示使用免费)。这样,每一条我们实际上了两次:一次是正常(同层内),一次是0权(层间)。 因此,另一种图方式是: for i in 0 to m-1: u, v, w = 输入 for layer in 0 to k: // 同层内双向(正常通过,不消耗免费次数) graph[u+layer*n].push_back({v+layer*n, w}); graph[v+layer*n].push_back({u+layer*n, w}); // 如果当前不是后一层,则层间(消耗一次免费机会,0权) if layer < k: // 使用免费,则从当前层的u到下一层的v,0权 graph[u+layer*n].push_back({v+(layer+1)*n, 0}); graph[v+layer*n].push_back({u+(layer+1)*n, 0}); 注意:这样图,每一条在同层内了双向,同时又在层间了双向0权?但是实际上,我们只需要单向的层间(因为免费只能从当前层到下一层,不能反向)。所以这里的层间也是单向的(从当前层指向下一层),而且了两个方向:u->v'和v->u'。这样,无论从u还是v出发,都可以使用免费机会到达下一层的对应节点。 因此,两种方式都可以。但是,上面这种方式实际上和前面第一种方式是一样的。 然而,我们观察:这样图后,同层内的是双向的(表示这条可以双向通行,且不消耗免费次数),而层间是单向的(从当前层到下一层,且0权),并且也是双向的(因为原图是无向图,所以我们在两个方向都了层间)。这样,就可以保证图的连通性。 所以,我们按照上述伪代码编写即可。 但是,注意:题目中起点和终点是给定的s和t。我们只需要从s(第0层)出发,到任意一层的t(即t+i*n)即可。 后,注意:有可能不使用免费机会就是优解,所以我们要考虑0到k层。 另外,有可能k次免费机会用不完,所以我们要在所有层(0到k)的终点中取小值。 下面给出一个C++的代码框架(实际提交代码需要根据题目调整): ```cpp #include <iostream> #include <vector> #include <queue> #include <cstring> using namespace std; const int MAXN = 10010; const int MAXK = 11; const int INF = 0x3f3f3f3f; int n, m, k, s, t; int total_nodes; vector<pair<int, int>> graph[MAXN * MAXK]; // 注意:总节点数为n*(k+1),k大为10,所以MAXK=11,MAXN=10010,总节点数110110,数组大小要够 int dis[MAXN * MAXK]; void dijkstra(int start) { memset(dis, INF, sizeof(dis)); dis[start] = 0; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; pq.push({0, start}); while (!pq.empty()) { int d = pq.top().first; int u = pq.top().second; pq.pop(); if (d != dis[u]) continue; for (auto &edge : graph[u]) { int v = edge.first; int w = edge.second; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; pq.push({dis[v], v}); } } } } int main() { cin >> n >> m >> k; cin >> s >> t; total_nodes = n * (k+1); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; // 对于每一层 for (int layer = 0; layer <= k; layer++) { int uu = u + layer * n; int vv = v + layer * n; // 同层内双向 graph[uu].push_back({vv, w}); graph[vv].push_back({uu, w}); // 如果当前不是后一层,再层间(单向,从当前层指向下一层,0权) if (layer < k) { // 从当前层的u到下一层的v graph[uu].push_back({v + (layer+1)*n, 0}); // 从当前层的v到下一层的u graph[vv].push_back({u + (layer+1)*n, 0}); } } } dijkstra(s); // 起点是第0层的s int ans = INF; for (int i = 0; i <= k; i++) { ans = min(ans, dis[t + i * n]); } cout << ans << endl; return 0; } ``` 但是,注意:上面的图方式会导致同层内的被重复?比如,对于同一层,一条(u,v)会被两次:一次是uu->vv,一次是vv->uu。然后层间也是,每个节点都会两条。这没有问题,因为是无向图。 但是,上面代码中,同层内我们了双向,层间了双向(实际上是两个方向的单向)。但是,层间我们只了当前层到下一层,没有下一层到当前层的(因为免费机会只能使用不能退回),所以是单向的。 然而,我们发现在同层内已经可以双向通行了,所以层间在两个方向都了(即从u到下一层的v,以及从v到下一层的u),这样在下一层内,我们可以通过同层内的双向再走,所以是正确的。 但是,上面的代码有一个问题:在层间部分,我们的是0权,但是注意,题目中免费乘坐,所以权为0。 但是,我们这样图,会导致数巨大,但题目数据范围(k<=10, m<=50000)可以接受。 另外,注意:节点编号是从0开始的,所以我们在输入时要注意题目给的节点编号是否从0开始。洛谷题目中,节点编号是0~n-1。 但是,上述代码在层间部分,我们的是0权,但是同层内是原权值。这样,当我们在同一层内行走时,花费正常累加;当使用免费机会时,通过0权进入下一层,不增加花费。 注意:我们图时,每一层内部都了完整的原图(包括双向),而层间则是在相邻层之间立的单向0权(两个方向都了,因为原图是无向图,所以两个方向都要)。 但是,这样图后,从第0层到第k层,我们可以通过多次使用免费机会到达更高的层,且每次使用免费机会都是通过0权。 因此,上述代码可以解决此问题。 但是,我们还需要注意:有可能存在重?题目没有说,所以按常规,重我们不需要特殊处理,因为Dijkstra算法会自然选择短的(当有重时,我们图会多条,然后松弛时会选择短的路径)。 但是,上面的代码中,对于同一条,我们在每一层都了,而层间了,所以重不影响。 但是,我们还可以优化图:实际上,同层内的对于每一层都是一样的,我们可以只一层,然后层间一次?不对,因为每一层内部都是独立的,所以必须每一层都。 所以,按照上述代码即可。 但是,我们注意到,在层间部分,我们的是0权,而题目中免费乘坐,所以0权是正确的。 后,注意:起点是s(第0层),终点是t(任意一层),所以我们在后比较的是t, t+n, t+2n, ... , t+k*n。 但是,有可能存在不连通的情况?题目保证有解,所以不用考虑。 另外,还有一种情况:可能到达终点的层数小于k,所以我们在所有层中取小值。 因此,我们输出所有层终点的小值。 但是,上述代码在洛谷上可能会超时?因为总数大约为 (4k+2)*m,当k=10时,4*10+2=42,m大50000,则数约为2100000,节点数为10000*11=110000,Dijkstra算法的时间复杂度为O((n+m)log n),这里n=110000, m=2100000,所以总操作次数大约是2210000*log(110000) ≈ 2210000 * 17 ≈ 37570000,在C++中可能勉强通过(注意常数优化)。 我们可以尝试优化:使用邻接表图,并且使用优先队列(小根堆),应该可以通过。 另外,我们还可以使用分层图的一种常见优化:不显式图,而是在Dijkstra中动态转移状态。即状态为(节点, 使用免费次数),然后对于每个节点,我们可以选择使用免费或者不使用免费。这样,我们不需要实际立多层图的节点,而是通过状态扩展。 具体做法: 定义dis[i][j]表示到达节点i,使用了j次免费机会的小花费。 然后,从起点(s,0)开始,对于当前状态(u, c),我们有两种选择: 1. 对于一条从u到v的,权值为w,不使用免费:那么状态转移到(v, c),花费dis[u][c]+w。 2. 如果c<k,使用免费:那么状态转移到(v, c+1),花费dis[u][c](因为免费,所以不加w)。 注意:无向图,所以每个节点的邻居都要考虑。 这样,我们相当于在状态空间上做Dijkstra。状态数为n*(k+1),数:每个状态u会尝试其所有邻居,然后有两种选择(使用免费或不使用),所以每个状态多扩展2*deg(u)条?实际上,对于每个邻居,我们有两种选择?不对,我们有两种选择:使用免费或不使用。所以对于每条,我们有两种转移方式。 代码框架: dis[u][c]: 表示到达节点u,使用了c次免费机会的小花费。 初始化:dis[s][0]=0,其他为INF。 优先队列:存 (花费, 节点, c) 每次取出花费小的状态。 遍历当前节点的所有邻居v,权为w: 1. 不使用免费:则新花费 = dis[u][c] + w,状态(v, c) 2. 如果c<k,使用免费:则新花费 = dis[u][c],状态(v, c+1) 然后松弛。 这种方法和分层图图本质相同,但避免了显式多层图,代码更简洁。 下面给出这种方法的伪代码: ```cpp #include <iostream> #include <vector> #include <queue> #include <cstring> using namespace std; const int MAXN = 10010; const int MAXK = 11; const int INF = 0x3f3f3f3f; int n, m, k, s, t; vector<pair<int, int>> graph[MAXN]; // 原图的邻接表 int dis[MAXN][MAXK]; // dis[i][j]: 到节点i,使用了j次免费机会的小花费 struct Node { int d, u, c; // 当前花费,节点,使用免费次数 Node(int d, int u, int c) : d(d), u(u), c(c) {} bool operator>(const Node& other) const { return d > other.d; } }; void dijkstra() { memset(dis, INF, sizeof(dis)); dis[s][0] = 0; priority_queue<Node, vector<Node>, greater<Node>> pq; pq.push(Node(0, s, 0)); while (!pq.empty()) { Node node = pq.top(); pq.pop(); int d = node.d, u = node.u, c = node.c; if (d != dis[u][c]) continue; for (auto &edge : graph[u]) { int v = edge.first, w = edge.second; // 不使用免费 if (dis[u][c] + w < dis[v][c]) { dis[v][c] = dis[u][c] + w; pq.push(Node(dis[v][c], v, c)); } // 使用免费(如果还有免费次数) if (c < k) { if (dis[u][c] < dis[v][c+1]) { dis[v][c+1] = dis[u][c]; pq.push(Node(dis[v][c+1], v, c+1)); } } } } } int main() { cin >> n >> m >> k; cin >> s >> t; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; graph[u].push_back({v, w}); graph[v].push_back({u, w}); // 无向图 } dijkstra(); int ans = INF; for (int i = 0; i <= k; i++) { ans = min(ans, dis[t][i]); } cout << ans << endl; return 0; } ``` 这种方法更常用,且节省内存(不需要立多层图,只需要原图),同时数没有增加,每个状态扩展的次数和原图中节点的度有关。 注意:在松弛时,不使用免费:转移到(v, c),使用免费:转移到(v, c+1)(如果c<k)。 所以,推荐使用这种方法。 但是,注意:无向图,所以原图的邻接表要双向。 在洛谷上,由于k<=10,n<=10000,所以状态数多为10000*11=110000,优先队列的操作次数大约为O(状态数*log(状态数)),可以接受。 因此,我们使用第二种方法(动态规划+短路)来解决此题。 下面,我们给出完整的洛谷P4568的AC代码(注意输入输出,题目要求): 注意:题目中节点编号0~n-1,可能有重和自环?题目没有说,但一般不会有自环,重的话,我们图时直接加入,Dijkstra会处理重(因为每次松弛都是取小花费)。 但是,注意:重在邻接表中会存储多次,然后我们每次枚举所有出,所以重会被多次考虑,自然取小值。 代码(C++): </think>### 洛谷P4568飞行路线分层图短路解决方案 #### 核心思路:分层图短路 分层图短路的核心思想是将原图复制成 $k+1$ 层($k$ 为免费次数),每层表示使用不同次数的免费机会。层内保留原权值,层间通过 **0权** 连接,表示使用免费机会。终答案在终点 $t$ 的所有分层中取小值。 #### 算法实现(动态规划+Dijkstra) ```cpp #include <iostream> #include <vector> #include <queue> #include <cstring> using namespace std; const int MAXN = 10010; const int MAXK = 11; const int INF = 0x3f3f3f3f; int n, m, k, s, t; vector<pair<int, int>> graph[MAXN]; // 原图邻接表 int dis[MAXN][MAXK]; // dis[i][j]: 到达节点i使用j次免费的小花费 struct Node { int d, u, c; // 当前花费、节点、使用免费次数 Node(int d, int u, int c) : d(d), u(u), c(c) {} bool operator>(const Node& other) const { return d > other.d; } }; void dijkstra() { memset(dis, 0x3f, sizeof(dis)); // 初始化为极大值 dis[s][0] = 0; priority_queue<Node, vector<Node>, greater<Node>> pq; pq.push(Node(0, s, 0)); while (!pq.empty()) { Node node = pq.top(); pq.pop(); int d = node.d, u = node.u, c = node.c; if (d != dis[u][c]) continue; // 已处理更优解 for (auto &edge : graph[u]) { int v = edge.first, w = edge.second; // 不使用免费机会 if (dis[u][c] + w < dis[v][c]) { dis[v][c] = dis[u][c] + w; pq.push(Node(dis[v][c], v, c)); } // 使用免费机会(需有剩余次数) if (c < k && dis[u][c] < dis[v][c+1]) { dis[v][c+1] = dis[u][c]; pq.push(Node(dis[v][c+1], v, c+1)); } } } } int main() { cin >> n >> m >> k >> s >> t; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; graph[u].push_back({v, w}); graph[v].push_back({u, w}); // 无向图双向 } dijkstra(); int ans = INF; for (int i = 0; i <= k; i++) { ans = min(ans, dis[t][i]); // 取所有分层的小值 } cout << ans << endl; return 0; } ``` #### 关键点解析 1. **状态定义** $dis[i][j]$ 表示到达节点 $i$ 时使用 $j$ 次免费机会的小花费。 2. **状态转移** - **不使用免费**:$dis[v][c] = \min(dis[v][c], dis[u][c] + w)$ - **使用免费**:$dis[v][c+1] = \min(dis[v][c+1], dis[u][c])$(需 $c < k$) 3. **时间复杂度** $O((n \cdot k) \log(m \cdot k))$,满足题目数据范围($n \leq 10^4, k \leq 10$)。 #### 分层图示例 对于原始 $(u,v,w)$: - **层内**:保留权值 $w$(图中实线) - **层间**:添加 $0$ 权(图中虚线) ``` 第0层(未使用免费):u0 --w-- v0 ↓0 ↓0 第1层(使用1次免费):u1 --w-- v1 ↓0 ↓0 ... ... ``` 通过层间 $0$ 权实现免费转移,终取终点 $t$ 在所有层的小值[^1][^2][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值