#### [$P9751$ [$CSP-J$ $2023$] 旅游巴士](https://www.luogu.com.cn/problem/P9751) ### 零:前导知识 https://www.bilibili.com/video/BV1ha4y1T7om/?vd_source=13b33731bb79a73783e9f2c0e11857ae ### 办法一:魔改$Dijkstra$ ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311080814008.png) 按测试用例进行分析题意: $1->2->5$是走$2$步,不满足终点是$3$的倍数条件,而我们可以调整的余地也不大,只能是在起点时多等待$3*m$个时间后出发,在路径上其它点都不能进行等待。 那么,走到$5$号节点,原来是$2$,现在就是$2+3*m$的时间点,根据我们已有数学知识知道,这是不可能$\%3=0$的! $1->3->4->5$是走$3$步,也就是$\%3=0$,那么,不就行了吗?不行!为什么呢?因为你看$4$号节点,如果从$1$节点在$0$时间出发,到达$3$号节点时是时间$1$,那么,$4$号节点要求时间$2$才能开放,你过不去啊! 怎么办?你只能是多等一个时间周期,也就是在节点$1$出发时不在$0$时刻出发,而是在$0+3$时间出发!这样,你在走$4$号节点时,就满足了$3>2$,就可以走了啊! 那问题来了,如果某个节点的时间要求不是数字$2$,而是$x$该怎么办呢? 其实我们需要保证到$u$号节点后,时间应该是$t>=x$的,如果现在$t$不是这样,那么需要一点点增大 ```cpp {.line-numbers} while(t, greater> q; //小顶堆 q.push({0,start}); //出发点入队列 d[start] = 0; //出发点距离0 while (q.size()) { auto t = q.top(); q.pop(); int u = t.second; if (st[u]) continue; st[u] = true; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; if (d[j] > d[u] + w[i]) { d[j] = d[u] + w[i]; q.push({d[j], j}); } } } } ``` 那就整一个三元组吧: ```cpp {.line-numbers} struct Node { int u, r, d; // u节点编号,r状态,d最短到达时间 // 重载 < ,优先队列,默认使用大顶堆,我们需要小顶堆,所以,需要重载小于号,描述两个Node对比, // 距离大的反而小,由于是大顶堆,所以距离小的在上 bool operator<(const Node t) const { return d > t.d; } }; ``` 其它的就是一个$Dijkstra$模板了。对了,还有最重要的一个问题: 这个距离$d$,在原始版本中代表的是到达某个节点的最短距离,现在的题目中是 - 最短距离 - 到达某个节点+节点的`%3`状态 比如现在倒数第二个节点最短距离是$8$,是通过$1$步过来的,记为$d(u,1)=8$ 那么它对于终点就是没用的,不如最短距离是$9$,通过频数是$2$的,也就是$d(u,2)=9$ 因为人家再走一步,就到终点了,你那个$8$使不上! ```cpp {.line-numbers} #include using namespace std; const int N = 10010, M = N << 1, K = 110; // 节点个数上限,边数上限,发车间隔时长上限 const int INF = 0x3f3f3f3f; // Dijsktra专用正无穷 struct Node { int u, r, d; // u节点编号,r状态,d最短到达时间 // 重载 < ,优先队列,默认使用大顶堆,我们需要小顶堆,所以,需要重载小于号,描述两个Node对比, // 距离大的反而小,由于是大顶堆,所以距离小的在上 bool operator<(const Node t) const { return d > t.d; } }; // 链式前向星 int e[M], h[N], idx, w[M], ne[M]; void add(int a, int b, int c = 0) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } /* dis[u][i]表示到达第u处地点,并且到达时间mod k = i的情况下的最短距离 */ int dis[N][K], st[N][K]; int main() { memset(h, -1, sizeof h); int n, m, k; cin >> n >> m >> k; for (int i = 0; i < m; i++) { int a, b, c; cin >> a >> b >> c; add(a, b, c); // 从a到b建一条边,c权值为开放时间 } // 初始状态,1号点在状态0时最短距离为0,其它点的最短距离为无穷大 memset(dis, 0x3f, sizeof dis); // dis是一个二维结果数组,一维是节点号,二维是此节点在不同状态下可以到达的最短路径 dis[1][0] = 0; // 所谓状态,是指到达时间(对于1号节点而言就是出发时间)%k的余数值 // 因为题目中说到,1号点和n号点都必须是%k=0的时间,所以,dis[1][0]=0 // 描述1号节点,在状态0下出发,距离出发点的距离是0 // 堆优化版Dijkstra求最短路,注意默认大顶堆,自定义比较规则 priority_queue q; // 初始状态加入优先队列,{点,状态,最短到达时间} q.push({1, 0, dis[1][0]}); // 节点号,%k的值,已经走过的距离 // 每个进队列的节点,都有3个属性:节点号,%k的值,已经走过的距离 while (q.size()) { // 从1号点开始宽搜 int u = q.top().u; // 节点编号u int r = q.top().r; // 节点状态r q.pop(); if (st[u][r]) continue; // 该状态已经加入到集合中,也就是已经被搜索过 // 先被搜索过在队列宽搜中意味着已经取得了最短路径 st[u][r] = 1; for (int i = h[u]; ~i; i = ne[i]) { // 枚举邻接点v和连接到v节点道路的开放时间 int v = e[i]; // v节点,也就是下一个节点 int t = w[i]; // v节点的开放时间 int d = dis[u][r]; // 到达(u,p)这个状态时的最短距离d int j = (r + 1) % k; // v节点的状态应该是u节点的状态+1后再模k // 如果到达时间小于开放时间,则将到达时间向后延长若干个k的整数倍(向上取整) while (d < t) d += k; // 如果可以松弛到v点的时间 if (dis[v][j] > d + 1) { // 下一个节点v的j状态,可以通过(u,i)进行转移,那么可以用t+1更尝试更新掉dis[v][j] dis[v][j] = d + 1; q.push({v, j, dis[v][j]}); // 再用(v,j)入队列去更新其它节点数据,标准的Dijkstra算法 } } } if (dis[n][0] == INF) // 如果终点的模k=0状态存在数字,那么就是说可以获取到最短路径,否则就是无法到达为个状态 cout << -1 << endl; else cout << dis[n][0] << endl; return 0; } ``` ### 办法二:二分+宽搜 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/202311071051699.png) 疑问与解答: #### $Q$:为什么要建反图? 答:举个栗子,比如从$1$出发有如下路径 ```cpp {.line-numbers} #include using namespace std; const int N = 10010, M = N << 1, K = 110; // 链式前向星 int e[M], h[N], idx, w[M], ne[M]; void add(int a, int b, int c = 0) { e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++; } // 入队列的结构体 struct Node { int id; // 节点号 int r; // %k的余数 }; int dis[N][K]; // 最短路径 int n, m, k; // n个节点,m条边,时间是k的整数倍,即 %k=0 bool bfs(int mid) { memset(dis, -1, sizeof dis); queue q; dis[n][0] = mid * k; q.push({n, 0}); while (q.size()) { int u = q.front().id; int r = q.front().r; q.pop(); if (dis[u][r] == 0) continue; // 反着走,距离为0,就不需要继续走了,剪枝 for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; // 前序节点 int p = (r - 1 + k) % k; // 前序节点合法状态 if (~dis[v][p]) continue; // 如果前序点的合法状态不等于-1,表示已经被宽搜走过了,根据宽搜的理论,就是已经取得了最短距离,不需要再走 if (dis[u][r] - 1 < w[i]) continue; // 现在的最短距离是dis[u][r],那么前序的距离应该是dis[u][r]-1,那么,如果dis[u][r]-1> n >> m >> k; while (m--) { int a, b, c; cin >> a >> b >> c; add(b, a, c); } int l = 0, r = (1e6 + 1e4) / k, ans = -1; while (l < r) { int mid = l + r >> 1; if (bfs(mid)) { ans = mid * k; r = mid; } else l = mid + 1; } cout << ans << endl; return 0; } ```