CSP-J2023T4 旅游巴士(同余最短路)

题目链接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] ,我们在宽搜队列中也不能仅仅保存点的编号,同时也要保存点的状态:

cpp 复制代码
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代码

cpp 复制代码
#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 - 博客园

相关推荐
knighthood20013 分钟前
ros中仿真编写launch时robot_state_publisher,output参数
c++·ubuntu·ros
小林熬夜学编程5 分钟前
【Linux系统编程】第四十二弹---多线程编程全攻略:涵盖线程创建、异常处理、用途、进程对比及线程控制
linux·服务器·c语言·开发语言·c++
£suPerpanda14 分钟前
牛客周赛 Round65 补题DEF
开发语言·数据结构·c++·算法·深度优先·动态规划·图论
ao_lang18 分钟前
剑指offer第五天
python·算法·cpp
付宇轩19 分钟前
leetcode 173.二叉搜索树迭代器
算法·leetcode·职场和发展
L_cl20 分钟前
数据结构与算法——Java实现 54.力扣1008题——前序遍历构造二叉搜索树
算法·leetcode
KeithTsui21 分钟前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift
code .22 分钟前
C++各个版本的主要特性
开发语言·c++·现代c++
玛卡巴卡(努力学习版)32 分钟前
矩阵特殊打印方式
c++·算法·矩阵