Part 8.2 最短路问题

题单

P4779 【模板】单源最短路径(标准版)

思路

堆优化(优先队列)优化的dijkstra算法板子题。它的核心思想就是贪心,即局部最优解推广至全局最优解;具体做法是,每次找距离起点最近的且未被vis数组标记的点u,对这个点能到达的点v的距离起点的距离d[v]进行更新(d[v] = d [u] + w),更新条件是d[u] + w < d[v],如果可以更新且v点未被vis标记,那就把v以及d[v]压入元素以d由小到大的顺序存储的小根堆;需要注意的是,dijkstra的使用条件是求只含有非负边权的最短路和只含有非正边权的最长路;它的时间复杂度是,O(mlog(m))。下面对屑想过的一些问题进行自问自答:
Q:为什么某个点在被打上标记后就表示其到起点的最新距离已经更新完毕?
A反证法。假设某个点u在被vis打上标记后不是最短路,则至少存在1条其他通往u的路径还未被遍历完且该条路径使得d[u]更小,易得:这条路径上的除u以及在u之后被遍历的点之外的任意一个点的d都比d[u]小,则可得他们比u更接近队列顶部,即他们应该比u更先遍历到,即若u被打了标记,则这些点一定已经被打了标记,与假设矛盾!故某个点在被打上标记后就表示其到起点的最新距离已经更新完毕!
Q:为什么一个点明明只会被打上标记并基于这个点向外扩展一次,但是需要这个点还未被标记就要把它压入队列中,即某个点可能会被多次压入队列?
A:因为这是由我们从队列中取距离起点最近的点u我们将(node){v, d[v]}压入队列的操作决定的。即:在某个点v被标记之前,会有多条路径通向它,对于新的一条路径得到的d[v]比原先的d[v]小时,我们便会进行更新,同时把这个新的{v, d[v]}以压入队列。也就说第一次被压入的d[v]不一定时最小的,这也就使得第一次压入的d[v]不满足dijkstra贪心向外拓展的条件

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100005, maxm = 500005;
int n, m, s, cnt, head[maxn], d[maxn], vis[maxn];
struct edge // 链式前向星存图,一定要掌握。
{	
	int v, w, next;
} e[maxm];
struct node 
{
	int pos, dis;
	bool operator < (const node &a) const // 学会写))) 
	{
		return a.dis < dis; 
	} 
};
void add (int u, int v, int w) 
{
	e[++cnt].next = head[u], e[cnt].v = v, e[cnt].w = w;
	head[u] = cnt; 
}
priority_queue <node> q;
void Dijkstra () 
{
	memset (d, 0x7f7f7f7f, sizeof (d));
	d[s] = 0;
	q.push ((node) {s, 0});
	while (!q.empty ()) 
	{
		int u = q.top().pos;
		q.pop (); // 千万不要忘记将这个元素弹出队列!!!
		if (vis[u]) continue; // 一个点在达到最短路后只用向外拓展一次
		vis[u] = 1;	
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (d[v] > d[u] + w) d[v] = d[u] + w;
			if (!vis[v]) 
				q.push ((node) {v, d[v]}); // 只要是这个点的d被更新了,就压入队列因为无法保证哪一次更新后的d是最小的
		}
	} 
}
int main ()
{
	scanf ("%d %d %d", &n, &m, &s);
	int u, v, w;
	for (int i = 1; i <= m; i++) 
	{
		scanf ("%d %d %d", &u, &v, &w);
		add (u, v, w);
	}
	Dijkstra ();
	for (int i = 1; i <= n; i++) 
		printf ("%d ", d[i]);
	return 0;
}


收获

千千万万不要忘了q.pop ()
这个图是有向图还是无向图一定要注意,因为这会影响结构体数组e的定义大小,不注意就有可能寄在这上面

P3385 【模板】负环

前言

上文说了,时间复杂度优良的dijkstra算法的使用条件后,感到有一点失望。那有没有什么较优的算法去跑含有任意大小(正负皆可)边权的最短路呢?那就轮到Bellman-Ford的优化版本SPFA登场了。

思路

SPFA算法的核心思想是宽度优先搜索,即对于每一个点都要去它所连接的点走一遍;他的作用是,跑含有任意大小(正负皆可)边权的最短路并在此过程中判断负环(一条边权之和为负数的回路)是否存在;他的具体操作是,见代码XD;它的时间复杂度是,O(1)~O(VE),其中V是点的个数,E是边的个数

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2010, maxm = 3010;
// tot[i]记录的是i点与起点之间的点的个数(不包括i点)。如果有n个点,我们不难想到若无负环,求最短路过程中得到的tot[i]一定小于等于n - 1,因为在整个图是条链的时候tot[i]才有可能等于n - 1;故若有一个tot[i_0]大于等于n,则表明有负环。
int n, m, t, head[maxn], cnt, tot[maxn], vis[maxn], d[maxn];
struct edge 
{
	int v, w, next;
} e[maxm << 1];
void add (int u, int v, int w) 
{
	e[++ cnt].next = head[u], e[cnt].v = v, e[cnt].w = w;
	head[u] = cnt;
}
queue <int> q, emptyi;
bool SPFA () 
{
	q = emptyi;
	q.push (1), d[1] = 0, vis[1] = 1;
	while (!q.empty ()) 
	{
		int u = q.front ();
		q.pop (); 
		vis[u] =  0; // 表示在u这个点将要完成扩展。
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (d[v] > d[u] + w) 
			{
				d[v] = d[u] + w;
				tot[v] = tot[u] + 1; // 这个要理解
				if (tot[v] >= n) return true; // 判断负环
				if (!vis[v]) vis[v] = 1, q.push (v); // 如果v的最短距离被更新了,则说明它有可能会导致它所连的点距离起点的最短距离得到更新。于是,我们把这个点打上标记,表示我们一会要再搜一遍这个点,并把这个点推入队列。
			}
		}
	} 
	return false;
}
int main ()
{
	scanf ("%d", &t);
	for (int k = 1; k <= t; k++) 
	{
		memset (head, 0, sizeof (head));
		memset (vis, 0, sizeof (vis));
		memset (tot, 0, sizeof (tot));
		memset (d, 0x7f7f7f7f, sizeof (d));
		cnt = 0;
		scanf ("%d %d", &n, &m);
		int u, v, w;
		for (int i = 1; i <= m; i++) 
		{
			scanf ("%d %d %d", &u, &v, &w);
			if (w >= 0) 
				add (u, v, w), add (v, u, w);
			else 
				add (u, v, w);
		}
		if (!SPFA ()) printf ("NO\n");
			else printf ("YES\n");
	}
	return 0;
}


收获

一定要明白tot数组所表示的含义。
知晓负环的定义。

P5905 【模板】Johnson 全源最短路

前言

前面介绍的小根堆优化的dijkstraSPFA,都是单源最短路。那有没有用来跑全源最短路的最短路算法?如果有n个点,在没有负边权的情况下,我们可以通过跑n轮堆优化dijkstra来实现全源最短路,时间复杂度是O (nmlog (m)); 那如果存在负边权,我们要跑n轮SPFA吗?如果这样做的话,时间复杂度最差高达O(E * V^2),其中V是点的的个数,E是边的个数。那有没有更优的方法?这就轮到Johnson全源最短路登场了!

思路

正如前言所说,我们要使用SPFA代替dijkstra的原因是,存在了负边权.那么我们把负边权在一定过程中通过不影响正确的最短路的方式变为非负边,不就可以用跑n轮堆优化dijkstra来实现全源最短路了吗?那怎么可以不影响正确最短路呢?不影响正确最短路说明:我们在改变边权后,无论我们通过哪条道路从u走到v,这些道路引起的u到v的最短距离的改变值使一样的;即u到v的最短距离的改变值改变值与过程无关,只与u和v的某一种状态有关! 于是,我们可以联想到物理中的某个概念——势能。于是我们在原先的图的基础上建立一个超级原点0号点,并从0点向其他各点建立一个边权为0的边,然后用SPFA跑一边包含这n + 1个点的最短路,将他们与0点之间的最短距离储存到势能数组h中;并在此过程中判断是否存在负环。然后,根据性质h[v] <= h[u] + w我们把边权w转化为w + h[u] - h[v] >= 0,这样可以使得无论从哪条路径,得到的距离都是真正的距离+h[s] - h[t],其中s为起点,t为终点,再跑n轮堆优化dijkstra来实现全源最短路。最后不要忘了将得到的最短距离+h[v] - h[u]。时间复杂度为O(nm + nmlogm)。以下开始屑的自问自答:
Q:为什么要0点向其他各点都建一个边权为0的边,只建立一条0到i(i是1,2,3……n中的一个数)的边不好吗?
A:因为你只建一个不能保证所有的点的h数组都能得到更新。举个极端的例子,比如说,你把0和一个没有出边的点建边了。
Q:0点向其他各点都建一个边权为0的边,不会导致出现一种极端情况h[i]全为0吗?
A:会,但这又如何?根据SPFA算法的正确性,我们知道性质h[v] <= h[u] + w一定成立,进而不会使后续的dijkstra算法失去正确性/

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 3010, maxm = 6010;
int n, m;
//构建与链式前向星有关的代码
struct edge 
{
	int v, w, next;
} e[maxm + maxn];
int head[maxn], cnt;
void add (int u, int v, int w) 
{
	e[++ cnt] = {v, w, head[u]};
	head[u] = cnt;
} 
//构建与SPFA(优化Bellmford)有关的代码同时创建势能
queue <int> qi;
int vis[maxn], h[maxn], tot[maxn]; //h为势能数组 
void SPFA () 
{
	memset (h, 0x7f7f7f7f, sizeof (h));
	qi.push (0), vis[0] = 1, h[0] = 0;
	while (!qi.empty ()) 
	{
		int u = qi.front ();
		qi.pop ();
		vis[u] = 0;
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (h[v] > h[u] + w) 
			{
				h[v] = h[u] + w;
				tot[v] = tot[u] + 1;
				if (tot[v] >= n + 1) 
				{
					printf ("-1");
					exit (0);
				}
				if (!vis[v]) qi.push (v), vis[v] = 1;
			}
		}
	}
} 
//准备dijkstra与node结构体
struct node 
{
	int pos, dis;
	bool operator < (node a) const 
	{
		return a.dis < dis;
	}
}; 
priority_queue <node> q, emptyi;
long long d[maxn];
void dijkstra (int s) 
{
	q = emptyi;
	memset (vis, 0, sizeof (vis));
	memset (d, 0x7f7f7f7f, sizeof (d));
	q.push ((node) {s, 0});
	d[s] = 0;
	while (!q.empty ()) 
	{
		int u = q.top ().pos;
		q.pop ();
		if (vis[u]) continue;
		vis[u] = 1;
		for (int i = head[u]; i; i = e[i].next) 
		{
			int v = e[i].v, w = e[i].w;
			if (d[v] > d[u] + w) 
				d[v] = d[u] + w;
			if (!vis[v]) q.push ((node) {v, d[v]}); 	
		}
	}
}
long long ans;
int main ()
{
	scanf ("%d %d", &n, &m);
	int u, v, w;
	for (int i = 1; i <= m; i++) 
		scanf ("%d %d %d", &u, &v, &w),
		add (u, v, w);
	for (int i = 1; i <= n; i++) 
		add (0, i, 0);
	SPFA ();
	for (int u = 1; u <= n; u++) 
	{
		for (int i = head[u]; i; i = e[i].next) 
			e[i].w += (h[u] - h[e[i].v]);
	}
	for (int i = 1; i <= n; i++) 
	{
		ans = 0;
		dijkstra (i);
		for (int j = 1; j <= n; j++) 
			if (d[j] == d[0]) ans += (1ll * j * 1000000000);
			else ans += (1ll * j * (d[j] + h[j] - h[i]));
		printf ("%lld\n", ans);
	}
	return 0;
}

收获

参考资料
初步了解“超级原点”的用法

P1144 最短路计数

思路

本质上就是堆优化dijkstra板子题,但是又有所变式,详情见代码注释。

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000005, maxm = 2000005, MOD = 100003;
int head[maxn], cnt;
struct edge 
{
	int v, w, next;
} e[maxm << 1];
void add (int u, int v, int w) 
{
	e[++ cnt] = {v, w, head[u]};
	head[u] = cnt;
}
struct node 
{
	int pos, dis;
	bool operator < (node a) const 
	{
		return a.dis < dis;
	}
};
priority_queue <node> q;
int ans[maxn], d[maxn], vis[maxn];
// ans[i]记录的是可以得到i到起点最短距离的路径有多少条
void dijkstra () 
{
	memset (d, 0x7f7f7f7f, sizeof (d));
	d[1] = 0; ans[1] = 1;
	q.push ((node) {1, 0});
	while (!q.empty ()) 
	{
		int u = q.top ().pos;
		q.pop ();
		if (vis[u]) continue;
		vis[u] = 1;
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (d[v] == d[u] + w) 
				ans[v] = (ans[u] + ans[v]) % MOD; // 如果d[v] == d[u] + w,说明目前得到的可能使v到起点的最短距离的距离,已经被走到过了,于是使再加上这ans[u]种走法。
			else if (d[v] > d[u] + w) 
			{
				d[v] = d[u] + w;
				ans[v] = ans[u] % MOD; // 如果d[v]得到了更新,说明以前的那个d[v]肯定不对了,于是ans[v]赋值为ans[u] % MOD
				if (!vis[v]) q.push ((node) {v, d[v]}); 
			}
		}
	} 
	return;
}
int n, m;
int main ()
{
	scanf ("%d %d", &n, &m);
	int u, v;
	for (int i = 1; i <= m; i++) 
		scanf ("%d %d", &u, &v),
		add (u, v, 1), add (v, u, 1);
	dijkstra ();
	for (int i = 1; i <= n; i++)
		printf ("%d\n", ans[i]);
	return 0;
}

收获

要灵活,不要死记板子。

P1462 通往奥格瑞玛的道路

思路

~~这个题的题干真的是好抽象。~~简言之,就是在所有保证术士到达目的地的路径中,找每一条路径中单个城市收费的最大值,然后再找这些最大值中的最小值。见到这种问题,我们不难想到对单个城市收费的最大值进行二分答案因为这个量具有有界性,即其范围是[min {f_i}, max {f_i}];具有单调性,若这个量取A时不成立,那么它取比A小的任何数都不会成立;若取A成立,那么它取比A大的任何数都成立。于是我们用d[i]数组记录从起点到i的损失的最小血量,跑dijkstra,其中需要注意的是若某个点的f大于A则这个点不能去;若d[n] <= b 则表明这个单个城市收费的最大值A合法。时间复杂度是O(log (fmax - fmin) * log (m) * m)

代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 10005, maxm = 50005;
int n, m, b, f[maxn], l, r, ans = -1;
int head[maxn], cnt;
struct edge 
{
	int v, w, next;
} e[maxm << 1];
void add (int u, int v, int w) 
{
	e[++ cnt] = {v, w, head[u]};
	head[u] = cnt;
}
struct node 
{
	int pos;
	long long dis;
	bool operator < (node a) const 
	{
		return a.dis < dis;
	}
};
int vis[maxn];
long long d[maxn];
priority_queue <node> q, emptyi;
bool dijkstra (int fmax) 
{
	if (fmax < f[1]) return false; // 特判连起点都不能去的情况。
	memset (d, 0x7f7f7f7f, sizeof (d));
	memset (vis, 0, sizeof (vis));
	q = emptyi;
	d[1] = 0;
	q.push ((node) {1, 0});
	int u;
	while (!q.empty ()) 
	{
		u = q.top().pos;
		q.pop (); 
		if (vis[u]) continue;
		vis[u] = 1;
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (f[v] > fmax) continue; // 大于fmax的点去不了
			if (d[v] > d[u] + w) 
			{
				d[v] = d[u] + w;
				if (!vis[v]) q.push ((node) {v, d[v]});
			} 
		} 
	} 
	if (d[n] > b) return false;
	else return true;
}
int main ()
{
	scanf ("%d %d %d", &n, &m, &b);
	l = 1e9 + 5;
	for (int i = 1; i <= n; i++) 
	{
		scanf ("%d", &f[i]);
		r = max (f[i], r);
		l = min (f[i], l);
	}
	int u, v, w;
	for (int i = 1; i <= m; i++) 
	{
		scanf ("%d %d %d", &u, &v, &w);
		add (u, v, w), add (v, u, w);
	}
	while (l <= r) 
	{
		int mid = (l + r) >> 1;
		if (dijkstra (mid)) 
		{
			ans = mid;
			r = mid - 1;
		}
		else l = mid + 1;
	}
	if (ans == -1) printf ("AFK");
	else printf ("%d", ans);
	return 0;
}

收获

二分答案与dijkstra的结合真是赏心悦目啊

A题 P1266 速度限制

B题 P4568 [JLOI2011] 飞行路线

前言

A、B两道题都涉及一个知识点——分层图(的参考学习博客)

A题思路

A题的主要难点在于当这条边的要求的速度为0即没有限速时,车在这条路上的速度与其从哪条道路来到这条边的起点有关。于是,我们需要用分层图的知识去解决这件事。

A题代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 305, maxm = 225050, maxvi = 505;
int n, m, d;
int head[maxn], cnt;
struct edge 
{
	int v, vi, l, next; // v是点,vi是速度 
} e[maxm]; // 错把m写成n,于是喜提1小时debug
void add (int u, int v, int vi, int l) 
{
	e[++ cnt] = (edge) {v, vi, l, head[u]};
	head[u] = cnt;
}
struct node 
{
	int pos, vi;
};
node from[maxn][maxvi]; // from[x][v]记录的是以速度v到达x点是从哪一个点以哪一个速度转移过来的。定义这个数组的作用是,便于输出最后的路径。有人可能会问from[x][v]可能会被覆盖啊,但是根据题目的提示,最快路径只有一条!
queue <node> q;
int vis[maxn][maxvi]; // 增加一维,vis[x][v]是用来给以v到达x的状态打标记的
double dis[maxn][maxvi]; // 增加一维,dis[x][v]记录的是以v到达x的最小时间
void spfa () 
{
	memset (from, -1, sizeof (from)); // 原来结构体数组也可以memset
	memset (dis, 127, sizeof (dis));
	vis[0][70] = 1, dis[0][70] = 0;
	q.push ((node) {0, 70});
	while (!q.empty ()) 
	{
		int u = q.front().pos, vi = q.front().vi;
		q.pop ();
		vis[u][vi] = 0;
		int v, vii, l;
		for (int i = head[u]; i; i = e[i].next) 
		{
			l = e[i].l, v = e[i].v, vii = e[i].vi;
			if (!vii) vii = vi; // 如果这条边要求的速度为0,则汽车的速度就是驶入u的速度
			if (dis[v][vii] > dis[u][vi] + 1.0 * l / vii) 
			{
				dis[v][vii] = dis[u][vi] + 1.0 * l / vii;
				if (!vis[v][vii]) vis[v][vii] = 1, q.push ((node) {v, vii});
				from[v][vii].pos = u, from[v][vii].vi = vi; // 
			}
		} 
	} 
}
void print (int x, int vi) // 递归输出答案,一定要明白!
{
	if (from[x][vi].pos != -1) print (from[x][vi].pos, from[x][vi].vi);
	printf ("%d ", x);
}
int main ()
{
	scanf ("%d %d %d", &n, &m, &d);
	int a, b, v, l;
	for (int i = 1; i <= m; i++) 
	{
		scanf ("%d %d %d %d", &a, &b, &v, &l);
		add (a, b, v, l);
	}
	spfa ();
	int minvi = 0;
	for (int i = 1; i <= 500; i++) // 先找到一哪个速度驶入d使得最快
	{
		if (dis[d][minvi] > dis[d][i])
			minvi = i;
	}
	print (d, minvi); // 递归输出答案
	return 0;
}


A题收获

掌握递归输出路径的方式
原来结构体数组也可以memset……

B题思路

较简单,直接上代码吧。

B题代码

#include <bits/stdc++.h>
using namespace std;
const int maxn = 20005, maxm = 50005, maxk = 15;
int n, m, k, s, t;
int head[maxn], cnt;
struct edge 
{
	int v, w, next;
} e[maxm << 1];
void add (int u, int v, int w) 
{
	e[++ cnt] = {v, w, head[u]};
	head[u] = cnt;
}
struct node 
{
	int pos, d, tot; // tot表示已使用了多少次免票机会
	bool operator < (node a) const 
	{
		return a.d < d;
	}
};
priority_queue <node> q;
int vis[maxn][maxk], dis[maxn][maxk];
// 其第二维都表示的是使用了多少次免票机会
void dijkstra () 
{
	memset (dis, 0x7f7f7f7f, sizeof (dis));
	dis[s][0] = 0;
	q.push ((node) {s, 0, 0});
	while (!q.empty ()) 
	{
		int u = q.top().pos, tot = q.top().tot;
		q.pop (); 
		if (vis[u][tot]) continue;
		vis[u][tot] = 1;
		int v, w;
		for (int i = head[u]; i; i = e[i].next) 
		{
			v = e[i].v, w = e[i].w;
			if (dis[v][tot] > dis[u][tot] + w) 
			{
				dis[v][tot] = dis[u][tot] + w;
				if (!vis[v][tot]) q.push ((node) {v, dis[v][tot], tot}); 
			}
			if (tot < k) 
			{
				if (dis[v][tot + 1] > dis[u][tot]) 
				{
					dis[v][tot + 1] = dis[u][tot];
					if (!vis[v][tot + 1]) q.push ((node) {v, dis[v][tot + 1], tot + 1});
				}
			}
		}  
	} 
}
int main ()
{
	scanf ("%d %d %d", &n, &m, &k);
	scanf ("%d %d", &s, &t);
	int a, b, c;
	for (int i = 1; i <= m; i++) 
	{
		scanf ("%d %d %d", &a, &b, &c);
		add (a, b, c), add (b, a, c);
	}
	dijkstra ();
	int ans = 2147483647;
	for (int i = 0; i <= k; i++) 
		ans = min (ans, dis[t][i]);
	printf ("%d", ans);
	return 0;
}


B题收获

觉得分层图的第二种做法好像动态规划

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值