「CQOI2005」新年好 - 最短路径

本文介绍了一道重庆省选题的算法解析,利用SPFA算法计算出从起点到五个目标点的最短路径,并通过遍历所有可能的访问顺序来找到总时间最短的路径。

题目描述

重庆城里有n个车站,m条双向公路连接其中的某些车站.每两个车站最多用一条公路连接,从任何一个车站出发都可以经过一条或者多条公路到达其他车站,但不同的路径需要花费的时间可能不同.在一条路径上花费的时间等于路径上所有公路需要的时间之和.
佳佳的家在车站1,他有五个亲戚,分别住在车站a,b,c,d,e.过年了,他需要从自己的家出发,拜访每个亲戚(顺序任意),给他们送去节日的祝福.怎样走,才需要最少的时间?

输入格式

第一行:n,m为车站数目和公路的数目.
第二行:a,b,c,d,e,为五个亲戚所在车站编号。
以下m行,每行三个整数x,y,t,为公路连接的两个车站编号和时间.

输出格式

仅一行,包含一个整数T,为最少的总时间.

样例输入

6 6
2 3 4 5 6
1 2 8
2 3 3
3 4 4
4 5 5
5 6 2
1 6 7

样例输出

21

数据规模与约定

1≤n≤500000,1≤m≤1000000;1\le n\le 500000,1\le m\le 1000000;1n500000,1m1000000;
1&lt;a,b,c,d,e≤n;1&lt;a,b,c,d,e\le n;1<a,b,c,d,en;
1≤x,y≤n,1≤t≤100;1\le x,y \le n,1 \le t\le 100;1x,yn,1t100;

分析

作为重庆省选题,这题算是比较水的了.可以先用spfa跑出a,b,c,d,e,1a,b,c,d,e,1a,b,c,d,e,1号节点与其它节点的最短路,再暴力枚举拜访顺序,求出最短的一条路径长度.

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define inf 0x7fffffff/2
using namespace std;
struct node {
	int to,next,wei;
}e[200005];
int h[50005],cnt,a[10];
int dt[50];
int d[7][50005],vis[50005];
int n,m,ans=inf;
void adg(int x,int y,int z) {
	e[++cnt]=(node){y,h[x],z};
	h[x]=cnt;
}
void spfa(int v0,int k) {
	queue<int> q;
	int book[50005]={0};
	q.push(v0);
	book[v0]=1;
	d[k][v0]=0;
	while (!q.empty()) {
		int w=q.front();
		q.pop();
		book[w]=0;
		for (int i = h[w];i;i=e[i].next) {
			int y=e[i].to;
			if (d[k][y]>d[k][w]+e[i].wei) {
				d[k][y]=d[k][w]+e[i].wei;
				if (!book[y]) {
					book[y]=1;
					q.push(y);
				}
			}
		}
	}
}
void dfs(int k) {
	if (k==6) {
		int s=0;
		for (int i = 1;i < 6;i++) {
			s+=d[dt[i]][a[dt[i+1]]];
		}
		ans=min(ans,s);
		return;
	}
	for (int i = 2;i <= 6;i++) {
		if (!vis[i]) {
			vis[i]=1;
			dt[k+1]=i;
			dfs(k+1);
			vis[i]=0;
		}
	}
}
int main() {
	scanf("%d%d",&n,&m);
	for (int i = 2;i <= 6;i++) {
		scanf("%d",a+i);
	}
	for (int i = 1;i <= m;i++) {
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		adg(a,b,c);
		adg(b,a,c);
	}
	memset(d,127,sizeof(d));
	a[1]=1;
	dt[1]=1;
	for (int i = 1;i <= 6;i++) {
		spfa(a[i],i);
	}
	dfs(1);
	printf("%d",ans);
	return 0;
}
<think>目要求:从车站1出发,依次拜访五个亲戚(顺序任意),求最少的总时间。 思路: 1. 由于拜访顺序任意,所以我们需要枚举所有可能的顺序(5! = 120种)。 2. 对于每种顺序,我们需要计算从起点1开始,依次经过这五个亲戚所在车站最短路径。 3. 计算任意两个亲戚车站之间的最短路径(包括起点1)?实际上,我们需要计算起点1和五个亲戚所在车站(共6个点)两两之间的最短路径。 但是注意:车站数量n最大50000,亲戚点只有6个(包括起点1,实际上起点1是固定的,另外五个亲戚点)。所以我们可以先预处理出这6个点到其他所有点的最短路径。 具体步骤: a. 我们记六个点为:start = 1, 以及另外五个点a,b,c,d,e(用数组relatives[5]存储)。 b. 分别以这六个点为起点,使用单源最短路径算法(如Dijkstra)计算到其他所有点的最短路径。这样我们得到了6个数组:dist[0][...], dist[1][...], ... , dist[5][...],其中dist[i][j]表示第i个点(即relatives[i])到车站j的最短路径。 c. 枚举五个亲戚的访问顺序(全排列),然后计算总路径: 总时间 = dist[0][relatives[perm[0]]] (这注意:起点1到第一个亲戚点的距离,但注意我们的dist[0]是起点1到其他点的距离,所以这应该是dist[0][relatives[perm[0]]]?不对,因为我们的六个点包括1和另外五个,所以我们需要新索引) 新组织: 六个点:points[0] = 1, points[1] = a, points[2] = b, points[3] = c, points[4] = d, points[5] = e. 然后我们预处理dist[i][j]表示从points[i]到j点的最短距离(j是任意点)。但是我们只需要知道这六个点两两之间的距离?不对,因为路径是连续的,但是中间经过的点不一定是这六个点,所以我们需要知道任意一个点到其他所有点的距离。但是,在计算总路径时,我们只需要知道从points[i]到points[j]的距离(因为路径是:1->第一个亲戚->第二个亲戚->...->第五个亲戚,而每个亲戚点都是points中的一个点)。 因此,我们实际上只需要计算六个点中任意两个点之间的最短路径?但是注意:两点之间可能有多路径,而且我们之前已经计算了每个点到其他所有点的距离,所以我们可以直接提取出两点之间的距离。 所以,我们可以构建一个6*6的矩阵graph,其中graph[i][j] = dist[i][points[j]],即从点points[i]到点points[j]的最短路径。但是注意,dist[i]是以points[i]为起点的最短路径数组,那么dist[i][points[j]]就是points[i]到points[j]的最短距离。 但是,这有一个问:我们计算了每个点到其他所有点的距离,所以graph[i][j]就是points[i]到points[j]的最短距离。 d. 枚举五个亲戚的排列(即1后面的5个点的排列),注意起点是0(因为起点是points[0]=1),然后顺序访问排列中的点(这些点对应points中的索引1~5)。那么总时间就是: total = graph[0][points[排列的第一个点索引]] + graph[排列的第一个点索引][points[排列的第二个点索引]] + ... 但是注意:排列中的点索引是1~5,所以排列中的第一个点实际上是points中的索引perm[0](取值1~5),第二个点是perm[1](也是1~5)...,所以: total = graph[0][perm[0]] // 因为graph[i][j]表示从points[i]到points[j]的距离,而points[perm[0]]就是第perm[0]个点,但这perm[0]是一个索引(1~5),所以我们需要用graph[0][perm[0]]?不对,graph[0][perm[0]]表示从points[0]到points[perm[0]]的距离,这确实是起点1到第一个亲戚点的距离。 但是,我们构建的graph是一个6*6的矩阵,graph[i][j]表示从点i到点j的最短距离,其中i和j都是0~5的索引(代表我们六个点)。所以,我们也可以新定义:graph[i][j]表示从第i个点到第j个点的最短距离(i,j是0~5的索引)。 因此,步骤: 1. 读入n, m,五个亲戚点,然后读入图。 2. 将六个点存入数组points: points[0]=1, points[1]=a, points[2]=b, points[3]=c, points[4]=d, points[5]=e。 3. 分别以这六个点为起点,运行Dijkstra算法,得到六个距离数组dist0, dist1, ... dist5。 4. 构建graph[6][6]:graph[i][j] = dist[i][points[j]] (注意:dist[i]是以points[i]为起点的距离数组,所以dist[i][points[j]]就是points[i]到points[j]的最短距离)。 5. 生成一个排列数组,表示五个亲戚点的访问顺序(注意:五个亲戚点对应索引1~5)。枚举1~5的全排列(120种)。 6. 对于每个排列,计算总路径: total = graph[0][排列的第一个索引] + graph[排列的第一个索引][排列的第二个索引] + ... + graph[排列的第四个索引][排列的第五个索引] 7. 取所有total的最小值。 注意:由于图是无向图,所以graph[i][j]等于graph[j][i],但这我们直接计算即可。 复杂度:6次Dijkstra,使用堆优化,每次O(m log n),m最大100000,n最大50000,6次就是600000*log50000,log50000约16,所以大约9.6e6,可以接受。 但是,注意:目中n最大50000,m最大100000,所以Dijkstra使用堆优化(优先队列)是可行的。 另外,由于有边,我们在建图时直接使用邻接表,并且Dijkstra中会自然处理边(因为每次松弛时,如果发现更小的距离就更新)。 实现细节: - 使用链式前向星(目要求不要vector,邻接数组)?但是目没有明确要求,但为了效率,我们使用邻接表(链式前向星)来存储图。 - 注意,图是无向图,每边存两次。 但是,我们不需要用链式前向星,因为目没有要求,而且使用vector方便,但考虑到效率(100000边,50000个节点)使用vector也是可以的。但是目要求不要vector?目描述中没有这个要求,但上一个目要求了,这个目没有要求。不过为了统一,我们使用链式前向星。 步骤: 1. 初始化链式前向星(静态邻接表)。 2. 读入m边,每边存两次(无向图)。 3. 定义points数组,存储6个点(起点1和五个亲戚点)。 4. 循环6次,每次以points[i]为起点运行Dijkstra,得到dist[i](数组,大小为n+1)。 5. 构建graph[6][6]:graph[i][j] = dist[i][points[j]] (注意,dist[i]是一个数组,dist[i][points[j]]表示从points[i]到points[j]的最短距离)。 6. 枚举索引1~5的全排列(可以用next_permutation)。 7. 计算每种排列的总距离,更新最小值。 注意:Dijkstra中,点的编号从1到n。 代码结构: 注意:由于有多个起点,我们用一个二维数组dists[6][MAXN]来存储?但是MAXN=50000,6*50000=300000,空间可以接受。 但是,我们也可以每次计算后,只记录graph[i][j]的值,而不保存整个数组?不行,因为我们需要dists[i]来获取graph[i][j](j从0到5)。所以,我们可以在每次Dijkstra后,将graph[i][0..5]的值提取出来(即从当前起点到其他5个点的距离),然后就可以释放当前dists数组?但是这样我们就不用保存整个dists数组了,只需要在每次Dijkstra后,将到其他5个点的距离存入graph[i][0..5]即可。这样,我们只需要一个dist数组(一维),复使用6次。 因此,步骤调整: for i in 0..5: 以points[i]为起点运行Dijkstra,得到dist数组(大小为n+1) 然后,对于j in 0..5: graph[i][j] = dist[points[j]] 这样,graph[i][j]就是点i到点j的最短距离。 然后,我们不再需要保存整个dist数组(只保留graph数组)。 全排列枚举:使用next_permutation,注意我们只需要枚举索引1,2,3,4,5的排列。所以,我们创建一个数组perm = {1,2,3,4,5},然后枚举所有排列。 总时间计算: total = graph[0][perm[0]]; total += graph[perm[0]][perm[1]]; total += graph[perm[1]][perm[2]]; total += graph[perm[2]][perm[3]]; total += graph[perm[3]][perm[4]]; 然后更新最小值。 注意:Dijkstra算法中,我们使用优先队列(小顶堆)。 由于n最大50000,m最大100000,所以Dijkstra算法使用堆优化是O(m log n),可以接受。 代码实现: 注意:链式前向星建图,无向图每边加两次。 链式前向星结构: struct Edge { int to, w, next; } edges[MAXM*2]; // 无向图,边数2*m int head[MAXN], edgeCount; void addEdge(int u, int v, int w) { edges[edgeCount] = {v, w, head[u]}; head[u] = edgeCount++; } 初始化:memset(head, -1, sizeof(head)); edgeCount=0; 然后读入边,每次addEdge(u,v,w)和addEdge(v,u,w)。 然后进行6次Dijkstra。 注意:Dijkstra算法中,使用一个dist数组(一维),每次计算前初始化为INF(一个很大的数)。 由于每次Dijkstra的起点不同,所以每次都需要新初始化dist数组。 使用优先队列(最小堆): priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; 或者:priority_queue<pair<int, int>> 用负数?这我们使用greater。 但是,由于我们使用链式前向星,遍历邻接边时,需要遍历head[u]开始的链表。 具体Dijkstra代码: void dijkstra(int start, int dist[]) { // 初始化dist数组,所有点距离为INF,起点为0 for (int i = 1; i <= n; i++) dist[i] = INF; dist[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, u = pq.top().second; pq.pop(); if (d != dist[u]) continue; // 跳过旧的、无效的距离 for (int i = head[u]; i != -1; i = edges[i].next) { int v = edges[i].to, w = edges[i].w; if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } } 但是,目要求不要vector?这优先队列使用了vector作为底层容器,但这是STL,应该可以接受。如果不允许,我们可以自己写堆?目没有这么严格,而且STL在竞赛中是允许的。 另外,注意:可能有边,但是链式前向星会存储多边,在松弛时,如果当前边提供更小的距离就会更新,所以边会被正确处理。 最后,注意内存:dists数组我们只需要一个(复使用6次),但是graph数组是6*6=36个整数,很小。 全排列枚举:使用next_permutation,需要包含<algorithm>。 注意:枚举排列前,先初始化一个数组:int perm[5] = {1,2,3,4,5}; 然后排序(虽然已经是1..5,但最好先排序),然后循环直到next_permutation返回false。 代码结构: int main() { // 读入n, m // 读入五个亲戚点 // 初始化图(链式前向星) // 读入m边 // 初始化points数组:points[0]=1, points[1..5]=a,b,c,d,e // 定义graph[6][6] // 定义dist数组(一维,大小n+1) // 循环i=0到5: // dijkstra(points[i], dist); // 计算以points[i]为起点的单源最短路径 // for j=0到5: // graph[i][j] = dist[points[j]]; // 初始化perm数组:{1,2,3,4,5} // int min_time = INF; // do { // int total = graph[0][perm[0]]; // for (int i=0; i<4; i++) { // total += graph[perm[i]][perm[i+1]]; // } // if (total < min_time) min_time = total; // } while (next_permutation(perm, perm+5)); // 输出min_time } 注意:perm数组中的元素是索引(1,2,3,4,5),对应points数组中的索引1..5(即五个亲戚点)。graph[0][perm[0]]:起点0(即points[0]=1)到perm[0]号亲戚点(即points[perm[0]])的距离。但是,perm[0]是一个整数(1~5),所以graph[0][perm[0]]就是graph[0][1]或graph[0][2]...?不对,graph[i][j]中的i和j都是0~5的索引。而perm数组中的每个元素是1~5,所以这我们直接使用即可。 但是,注意:graph[i][j]中的j是0~5的索引,而perm数组中的元素是1~5,所以我们需要用perm[i]作为graph的列索引?是的,因为graph[i][j]的j就是0~5的索引,而亲戚点对应的索引是1~5(因为points[0]是起点,points[1..5]是亲戚点)。所以,graph[0][perm[0]]:这的perm[0]是1~5,所以graph[0][1]表示起点0(即points[0])到索引1的点(即points[1])的距离,这是正确的。 但是,在graph[perm[i]][perm[i+1]]中,perm[i]是1~5,所以graph[1][2]表示从points[1]到points[2]的距离,也是正确的。 因此,代码正确。 但是,注意:全排列枚举的是五个亲戚点的顺序,所以总共有5!种,而起点0是固定的,所以第一个点就是perm[0](即第一个访问的亲戚点)。 最后,注意:总时间可能很大,但目保证T<=10^9,所以用int即可(int范围大约2e9)。 但是,INF的设置:由于t<=10000,最大距离可能为10^5 * 10000 = 10^9,所以INF可以设为0x3f3f3f3f3f3f3f3f?但是目保证T<=10^9,所以我们可以用0x3f3f3f3f(大约1e9)不够,所以INF设为0x3f3f3f3f3f3f3f3f(long long)?但是目说T<=10^9,所以我们用int的最大值?或者设为1e18?为了安全,我们使用long long的INF,但目保证T<=10^9,所以用int也可以(最大2e9),但为了保险,我们用long long来存储总时间,而dist数组可以用int(因为每边权值不超过10000,最坏情况10^5边,最大距离10^5*10000=10^9,所以用int存储距离可能溢出?所以dist数组用long long?或者用int,因为目保证T<=10^9,所以最短路不会超过10^9,所以用int可以,因为int在大多数机器上是2e9,足够)。 但是,目中n最大50000,m最大100000,最坏情况下,一路径最多100000边,每边最大10000,所以最大距离100000*10000=1e9,所以用int存储距离是足够的。 所以,我们定义dist数组为int,graph数组为int,总时间用int。 但是,在Dijkstra中,松弛时计算dist[u]+w,如果dist[u]是int,w是int(<=10000),那么最大可能1e9+10000,不会溢出。 所以,我们使用int。 但是,INF的设定:0x3f3f3f3f是1061109567,大于10^9(10^9=1000000000),所以0x3f3f3f3f不够(因为最大距离可能是1e9)。所以我们可以用0x7fffffff(INT_MAX)?但是这样在松弛计算时,dist[u]+w可能会溢出(变成负数)。 因此,我们使用一个比10^9大的值,比如0x3f3f3f3f3f3f3f3f,但这样dist数组需要long long?或者我们可以用0x3f3f3f3f不够,所以用0x7f7f7f7f(大约2139062143)?这样比10^9大,而且不会溢出(因为最大距离1e9,而0x7f7f7f7f>1e9)。但是0x7f7f7f7f是2139062143,大于10^9,可以。 所以,我们定义: const int INF = 0x7f7f7f7f; 但是,0x7f7f7f7f是2.1e9,大于10^9,所以可以。 但是,在松弛计算时,dist[u]是0x7f7f7f7f,加上w(最大10000)不会溢出(因为0x7f7f7f7f+10000 < 2147483647),所以可以。 因此,代码: 注意:如果某两个点不连通,那么graph[i][j]会是INF,那么总时间就会很大,不会影响最小值(因为目保证T<=10^9,而且目说从任何一个车站出发都可以经过一或者多公路到达其他车站,所以图是连通的?但是目只说了“从任何一个车站出发都可以经过一或者多公路到达其他车站”,所以图是连通的,所以不会出现INF的情况)。 所以,我们直接计算即可。 最后,注意:Dijkstra中,优先队列存储的是负值还是正值?我们使用greater,所以存储的是正数(距离,从小到大)。 代码: 由于目要求不要注释,所以代码中不写注释,但这解释清楚。 注意:链式前向星,无向图,边数要开2*m。 下面给出完整代码:</think>### 解决方案 #### 方法思路 为了解决这个问,我们需要找到从起点1出发,依次拜访五个亲戚的最短路径。由于拜访顺序可以任意,我们需要枚举所有可能的顺序(5! = 120种)。对于每种顺序,计算路径的总长度,并取最小值。具体步骤如下: 1. **预处理关键点之间的最短路径**:以起点1和五个亲戚点作为关键点,分别计算从每个关键点到图中所有其他点的最短路径(使用Dijkstra算法)。 2. **构建距离矩阵**:用矩阵存储关键点两两之间的最短路径距离。 3. **枚举拜访顺序**:生成五个亲戚点的所有排列顺序,计算每种顺序的总路径长度。 4. **输出结果**:取所有路径长度中的最小值作为答案。 #### 解决代码 ```cpp #include <iostream> #include <cstring> #include <queue> #include <algorithm> #include <vector> #include <climits> using namespace std; typedef pair<int, int> pii; const int MAXN = 50010; const int MAXM = 200010; const int INF = 0x3f3f3f3f; struct Edge { int to, w, next; } edges[MAXM * 2]; int head[MAXN], edgeCount = 0; int dist[6][MAXN]; int points[6]; bool visited[MAXN]; void addEdge(int u, int v, int w) { edges[edgeCount] = {v, w, head[u]}; head[u] = edgeCount++; } void dijkstra(int start, int dist[]) { priority_queue<pii, vector<pii>, greater<pii>> pq; dist[start] = 0; pq.push({0, start}); while (!pq.empty()) { int u = pq.top().second; pq.pop(); if (visited[u]) continue; visited[u] = true; for (int i = head[u]; i != -1; i = edges[i].next) { int v = edges[i].to, w = edges[i].w; if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; pq.push({dist[v], v}); } } } } int main() { ios::sync_with_stdio(false); cin.tie(0); memset(head, -1, sizeof(head)); int n, m; cin >> n >> m; points[0] = 1; for (int i = 1; i <= 5; i++) { cin >> points[i]; } for (int i = 0; i < m; i++) { int x, y, t; cin >> x >> y >> t; addEdge(x, y, t); addEdge(y, x, t); } memset(dist, 0x3f, sizeof(dist)); for (int i = 0; i < 6; i++) { memset(visited, false, sizeof(visited)); dijkstra(points[i], dist[i]); } int perm[5] = {1, 2, 3, 4, 5}; int min_time = INF; do { int total = dist[0][points[perm[0]]]; for (int i = 0; i < 4; i++) { total += dist[perm[i]][points[perm[i+1]]]; } if (total < min_time) min_time = total; } while (next_permutation(perm, perm + 5)); cout << min_time << endl; return 0; } ``` ### 代码解释 1. **链式前向星初始化**: - `head`数组初始化为-1,表示每个节点初始时没有边。 - `edges`数组存储所有边,每无向边存储两次(正向和反向)。 2. **Dijkstra算法**: - 以每个关键点(起点1和五个亲戚点)为起点,计算到图中所有其他点的最短路径- 使用优先队列(最小堆)优化,确保高效找到当前最短路径- 每次松弛操作更新距离,并将新距离加入优先队列。 3. **距离矩阵构建**: - `dist[i][j]`存储从关键点`points[i]`到节点`j`的最短距离。 - 预处理后,`dist[i][points[j]]`即为关键点`i`到关键点`j`的最短距离。 4. **全排列枚举**: - 生成五个亲戚点的所有排列顺序(120种)。 - 对每种顺序,计算路径总长度:起点到第一个亲戚点 + 各亲戚点之间的路径长度。 5. **结果输出**: - 取所有路径总长度的最小值作为答案输出。 ### 时间复杂度 - **Dijkstra算法**:运行6次,每次时间复杂度为$O(m \log n)$,总时间$O(6m \log n)$。 - **全排列枚举**:$O(5!)$,即120次。 - **总时间复杂度**:$O(6m \log n + 120)$,满足目要求。 ### 相关问
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值