题目链接:https://www.luogu.com.cn/problem/P9751
题意:给定 n 个点, m 条单向边,一个时间间隔 k 。有这样一些限制条件:
1)1号点是入口, n 号点是出口;
2)经过一条边的时间花费是1;
3)进入1号点和离开 n 号点的时刻都必须是 k 的整数倍(包括0),抵达其他点没有此限制;
4)每条边有开放时间 a_i ,在 a_i 时刻之前无法通过这条边;
5)不可以在任何一条边上或者点上停留,一旦从入口1号点进入后,就不能停下地走向 n 号点。
问从 n 号点离开的最早时刻是什么时候。如果不存在满足所有条件的旅行方案,输出-1。
数据范围: 2<=n<=10^4 , 2<=m<=2\times 10^4 , 1<=k<=100 , 0<=a_i<=10^6 。
样例模拟:
整体思路:
在不考虑有出发离开和道路开放的时间限制的情况下,这是一个求解单源最短路的图论题。求解单源最短路的算法普遍用的较多的有两种:
dijastra
spfa
但是需要注意到,在一个可行解的方案中,如果要保证离开n号点的时刻是k的整数倍,则倒数第二个节点抵达的时刻必须是模 k 等于 k-1 ,倒数第三个节点抵达的时刻必须是模 k 等于 k-2 …
所以要对图上每个节点做一个分层处理,按照抵达的时刻对 k 取模的结果分为 k 层,拆分成 k 个状态。
因本题没有负边权,所以选择哪种做法都可以,这里我们用是spfa算法来求解。
算法细节考虑:
1)如何分层处理呢?
朴素最短路做法中的 dis[u] 变量用于存储抵达到点 u 时的最短路径,这里我们对该变量做分层处理,用 dis[u][i]=t 表示抵达点 u 时的最少花费时间为 t ,并且满足 t% k=i 。
那么到达1号点的合法状态只有 dis[1][0] ,离开 n 号点的合法状态只有 dis[n][0] 。
另外记录点是否已经被访问过的状态数组 st[u] ,也同样要这样分层处理,裂变为 st[u][i] ,我们在宽搜队列中也不能仅仅保存点的编号,同时也要保存点的状态:
q.push(pair<int,int>{u, i});
2)如何处理边上有开放时间的限制呢?
从点 u 转移至点 v 时,需要判断当前边是否开放,不妨设此时到达点 u 的时间为 t ,那么存在以下两种情况:
a) t>=a_i ,此时抵达时间已经晚于开放时间了,所以可以直接通过,那么抵达点 v 的时刻计算方式为 dis[v][j]=t+1,j=(t+1)%k ;
b) t<a_i ,此时抵达时间早于开放时间,可以假定往后延迟 k 的整数倍的时间进入1号点,那么该方案状态下保存的路径依旧合法,只是路径上所有遍历过的点的抵达时间往后延迟 k 的整数倍(但实际上我们并不会去修改所有遍历过的点的 dis 的值,因为它们可以用来更新其他不用延迟的后继点),这样就可以保证抵达时间晚于开放时间了, dis[v][j]=t+\lceil (a_i-t)/k\rceil\times k+1,j=(t+\lceil (a_i-t)/k\rceil\times k+1)%k 。
至此,需要考虑的细节已梳理完成。
AC代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 10005; // 点数
const int M = 20005; // 边数
const int K = 105;
int n, m, k;
int h[N], e[M], w[M], ne[M], idx;
void add(int u, int v, int a) {
e[idx] = v, w[idx] = a, ne[idx] = h[u], h[u] = idx++;
}
// dis[v][i]代表抵达v点的最小花费时间为t,且t%k=i
// 那么起始状态必须是dis[1][0],终结状态必须是dis[n][0]
// 如果t>=ai,那么dis[v][j]=t+1,j=(t+1)%k
// 如果t<ai,那么可以往后延长k的整数倍时间,new_t=dis[v][j]=t+向上取整[(ai-t)/k]*k+1,j=new_t%k
int dis[N][K];
bool st[N][K];
// 有解返回最短路解,无解返回-1
int spfa() {
queue<PII> q;
memset(dis, 0x3f, sizeof dis);
dis[1][0] = 0, st[1][0] = true;
q.push({1, 0});
while(q.size()) {
PII p = q.front(); q.pop();
int u = p.first, i = p.second;
st[u][i] = false;
for (int x = h[u]; x != -1; x = ne[x]) {
int v = e[x], a = w[x];
int t = dis[u][i];
if (t < a) t += (a-t+k-1)/k * k;
int j = (t+1) % k;
if (dis[v][j] > t+1) {
dis[v][j] = t+1;
if (!st[v][j]) {
st[v][j] = true;
q.push({v, j});
}
}
}
}
if (dis[n][0] == 0x3f3f3f3f)
return -1;
return dis[n][0];
}
int main() {
// freopen("D.in", "r", stdin);
scanf("%d%d%d", &n, &m, &k);
memset(h, -1, sizeof h);
while (m--) {
int u, v, a;
scanf("%d%d%d", &u, &v, &a);
add(u, v, a);
}
printf("%d\n", spfa());
return 0;
}
延申阅读:
这类图有一个细化分类,叫做同余最短路。对于同余最短路还有动态规划的解法:
同余最短路的转圈技巧 - qAlex_Weiq - 博客园