最短路算法

最短路问题

给定起点 s 和终点 t ,在所有能连接 s 和 t 的路径中,寻找边的权值之和最小的路径,就是最短路径问题

相关资料:
https://oi-wiki.org/graph/shortest-path/


单源最短路-迪杰斯特拉算法

用于计算一个节点到其他所有节点的最短路径
Dijkstra 算法是贪心算法, 是一种求解非负权图上单源最短路径的算法。
基本思想是:设置一个顶点的集合S,并不断地扩充这个集合,当且仅当从源点到某个点的路径已求出时它才属于集合S。开始时S中仅有源点,调整S 集合之外的点的最短路径长度, 并从中找到当前最短路径点,将其加入到集合S,直到所有的点都在S中。
仅对非负权边图有效
对于 n 个顶点,m 条边的非负权图

  • 若为稀疏图,点和边的数量差不多,则利用优先队列优化找最近距离点
  • 若为稠密图,点少于边,则暴力\(O(n^2)\)找最近距离更加有效

注意一点:如果使用优先队列进行优化,记得使用最小堆!不然优先队列默认是最大堆!!!


模板题

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

qiansui_code

2.#119. 单源最短路

注意题目所给图为无向图
代码:
https://loj.ac/s/1803867 与上同
https://loj.ac/s/1803868 受到启发,改了一小点,此处无需给head数组赋初值

3.hdu 2544 最短路

2023-07-13 15:08:40 星期四

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=2e5+5,inf=0x3f3f3f3f,mod=998244353;
int n,m,cnt=1;
vector<int> head,dis;

struct edge{
	int to,next,w;
}p[maxm];

void add_edge(int a,int b,int c){
	p[cnt].to=b;
	p[cnt].next=head[a];
	p[cnt].w=c;
	head[a]=cnt++;
	return ;
}

void dij(int s){
	priority_queue<pii,vector<pii>,greater<pii>> q;
	q.push({0,s});
	pii t;
	vector<bool> vis(n+5,false);
	while(!q.empty()){
		t=q.top();
		q.pop();
		if(vis[t.second]) continue;
		vis[t.second]=true;
		dis[t.second]=t.first;
		for(int i=head[t.second];i;i=p[i].next){
			int v=p[i].to;
			if(!vis[v] && t.first+p[i].w<dis[v]){
				q.push({t.first+p[i].w,v});
			}
		}
	}
	return ;
}

void solve(){
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		head=vector<int> (n+5,0);
		dis=vector<int> (n+5,inf);
		cnt=1;
		int a,b,c;
		for(int i=0;i<m;++i){
			cin>>a>>b>>c;
			add_edge(a,b,c);
			add_edge(b,a,c);
		}
		dij(1);
		cout<<dis[n]<<'\n';
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

k短路径问题

(待补充)


Bellman-Ford算法

原理:对于一个有 n 个点的图,给每个点 n 次查询邻居的机会,判断是否有到起点 s 的更短的路径,如果有就更新;经过 n 轮的查询和更新,就得到了所有点到起点 s 的最短路径
\(s->u->v,dis[v]=min(dis[v],dis[u]+w[u,v])\)
不断对边进行松弛操作,实现最优

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 +1,而最短路的边数最多为 n-1,因此整个算法最多执行 n-1 轮松弛操作。故总时间复杂度为 \(O(nm)\)
但还有一种情况,如果从 S 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 n-1 轮,因此如果第 n 轮循环时仍然存在能松弛的边,说明从 S 点出发,能够抵达一个负环。
需要注意的是,以 S 点为源点跑 Bellman–Ford 算法时,如果没有给出存在负环的结果,只能说明从 S 点出发不能抵达一个负环,而不能说明图上不存在负环。因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman–Ford 算法。

相关资料

oi wiki Bellman-ford

模板题

板子,来自oi wiki

struct edge {
	int v, w;
};

vector<edge> e[maxn];
int dis[maxn];
const int inf = 0x3f3f3f3f;

bool bellmanford(int n, int s) {
	memset(dis, 63, sizeof(dis));
	dis[s] = 0;
	bool flag;  // 判断一轮循环过程中是否发生松弛操作
	for (int i = 1; i <= n; i++) {
		flag = false;
		for (int u = 1; u <= n; u++) {
			if (dis[u] == inf) continue;
			// 无穷大与常数加减仍然为无穷大
			// 因此最短路长度为 inf 的点引出的边不可能发生松弛操作
			for (auto ed : e[u]) {
				int v = ed.v, w = ed.w;
				if (dis[v] > dis[u] + w) {
					dis[v] = dis[u] + w;
					flag = true;
				}
			}
		}
		// 没有可以松弛的边时就停止算法
		if (!flag) break;
	}
	// 第 n 轮循环仍然可以松弛时说明 s 点可以抵达一个负环
	return flag;
}

  1. hdu 2544 最短路
//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=1e4+5,inf=0x3f3f3f3f,mod=998244353;
int n,m,cnt;
vector<int> dis;

struct edge{
	int u,v,w;
}p[maxm<<1];

void add_edge(int a,int b,int c){
	p[cnt].u=a;
	p[cnt].v=b;
	p[cnt++].w=c;
	return ;
}

void bellman_ford(int s){//本题够用,但是不完整
	dis=vector<int>(n+5,inf);
	dis[s]=0;
	for(int i=0;i<n;++i){
		for(int j=0;j<cnt;++j){
			int a=p[j].u,b=p[j].v;
			if(dis[a]>dis[b]+p[j].w){
				dis[a]=dis[b]+p[j].w;
			}
		}
	}
	return ;
}

void solve(){
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		int a,b,c;
		cnt=0;
		for(int i=0;i<m;++i){
			cin>>a>>b>>c;
			add_edge(a,b,c);
			add_edge(b,a,c);
		}
		bellman_ford(1);
		cout<<dis[n]<<'\n';
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

SPFA算法

基于Bellman-Ford算法的思路,但是利用一个队列来存有更新状态的顶点,去除无用状态的顶点实现优化

模板题

板子,来自oi wiki

struct edge {
	int v, w;
};

vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;

bool spfa(int n, int s) {
	memset(dis, 63, sizeof(dis));
	dis[s] = 0, vis[s] = 1;
	q.push(s);
	while (!q.empty()) {
		int u = q.front();
		q.pop(), vis[u] = 0;
		for (auto ed : e[u]) {
			int v = ed.v, w = ed.w;
			if (dis[v] > dis[u] + w) {
				dis[v] = dis[u] + w;
				cnt[v] = cnt[u] + 1;  // 记录最短路经过的边数
				if (cnt[v] >= n) return false;
				// 在不经过负环的情况下,最短路至多经过 n - 1 条边
				// 因此如果经过了多于 n 条边,一定说明经过了负环
				if (!vis[v]) q.push(v), vis[v] = 1;
			}
		}
	}
	return true;
}
  1. hdu 2544 最短路
//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=1e2+5,inf=0x3f3f3f3f,mod=998244353;
int n,m,cnt;
vector<int> dis;

struct edge{
	int v,w;
};

void spfa(int s,vector<edge> *e){
	dis=vector<int>(n+5,inf);
	vector<bool> vis(n+5,false);
	queue<int> q;
	q.push(s);
	dis[s]=0;vis[s]=true;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=false;
		for(auto x:e[u]){
			int v=x.v,w=x.w;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				if(!vis[v]){
					q.push(v);
					vis[v]=true;
				}
			}
		}
	}
	return ;
}

void solve(){
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		vector<edge> e[maxm];
		edge t;
		int a,b,c;
		for(int i=0;i<m;++i){
			cin>>a>>b>>t.w;
			t.v=b;
			e[a].push_back(t);
			t.v=a;
			e[b].push_back(t);
		}
		spfa(1,e);
		cout<<dis[n]<<'\n';
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

多源最短路-Floyd算法

朴素的\(O(n^3)\)求解所有节点之间的最短距离
当中转点固定时,也可以仅考虑内部的两重循环,基本思想不变
中转点放在三重循环最外层

for(int k=1;k<=n;++k){//枚举中转点
	for(int i=1;i<=n;++i){//枚举任意两点
		for(int j=1;j<=n;++j){
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
		}
	}
}

可用来判断负环的存在,因为在负环上每转一圈,总长度就更小。在算法的运行过程中只要出现任意\(dp[i][i]<0\),就说明有负环。
注意:若询问\(dp[i][i]\),则\(dp[i][i]=0\)
基本应用场景:n<300;问题的解决与中转点有关;可能多次查询不同点对之间的最短路径等


模板题

1.hdu 1385 Minimum Transport Cost
打印最短路径
可利用\(path[i][j]\)表示从 i 到 j 的最短路的下一个节点,用于最后的输出路径
注意本题的费用既包括城市之间的距离,还包括中转站之间的费用。还有就是最后的输出路径,当起点和终点相同的时候输出什么,怎么输出,都得仔细考虑考虑
利用floyd求解本题则即为容易,多源最短路问题
下为代码,做的时候wa在了奇奇怪怪的地方

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*
wa在哪里了啊?
真就爆ll了?
无语子
*/
const int maxm=2e5+5,inf=0x3f3f3f3f,mod=998244353;
int n;
vector<vector<ll>> a,path;
vector<ll> b;

void input(){
	a=vector<vector<ll>> (n+5,vector<ll>(n+5,inf));
	path=vector<vector<ll>> (n+5,vector<ll>(n+5));
	b=vector<ll> (n+5,0);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			cin>>a[i][j];
			if(a[i][j]==-1) a[i][j]=inf;
			path[i][j]=j;
		}
	}
	for(int i=1;i<=n;++i) cin>>b[i];
	return ;
}

void floyd(){
	for(int k=1;k<=n;++k){
		for(int i=1;i<=n;++i){
			for(int j=1;j<=n;++j){
				int lu=a[i][k]+a[k][j]+b[k];
				if(a[i][j]>lu){
					a[i][j]=lu;
					path[i][j]=path[i][k];
				}else if(a[i][j]==lu && path[i][j]>path[i][k]){//字典序最小的路
						path[i][j]=path[i][k];
				}
			}
		}
	}
	return ;
}

void output(){
	int x,y;
	while(cin>>x>>y){
		if(x==-1 && y==-1) break;
		cout<<"From "<<x<<" to "<<y<<" :\n"
			<<"Path: "<<x;
		for(int i=x;i!=y;i=path[i][y]){
			cout<<"-->"<<path[i][y];
		}
		cout<<"\nTotal cost : "<<a[x][y]<<"\n\n";
	}
	return ;
}

void solve(){
	while(cin>>n){
		if(n==0) break;
		input();
		floyd();
		output();
	}
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
//	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}

2.洛谷 p1119 灾后重建
这题不是纯粹的floyd,而是通过时间限制中转点什么时候可以作为中转点。我们可以对于时间排序,当询问某一时间 t 两村庄的最短路径时,应当已经计算出 0~t-1 时间任意两点间的最短路,利用floyd即可,我们采取逐步加入点的策略,如果当前时间已经大于等于某个村庄的重建时间,那么就将其加入最短路径的计算。
可以通过离线排序查询的方式来实现,不知有无更优的办法。
下为代码链接(写的丑就不贴了)
code_qiansui

3.将某一点作为中转点跑floyd:hdu 3631 Shortest Path

void floyd(int k){
	for(int i=0;i<n;++i){
		for(int j=0;j<n;++j){
			g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
		}
	}
	return ;
}

4.洛谷 P1613 跑路
利用 floyd 求解最短路。
点与点之间的路径长度需要通过倍增预先处理。初始的边长度为 1,通过下面的循环将两条 \(2^{l - 1}\) 的路径集合到 \(2^l\) 上来(题目特殊性)

if(lu[i][j][l - 1] && lu[j][k][l - 1]){
	lu[i][k][l] = true;
	da[i][k] = 1;
}

AC代码:洛谷 code


求解传递闭包问题

可以利用floyd算法判断图中任意两点之间的连通性
如果仅询问图中两点的连通性,用BFS或DFS即可,但是求所有点对,floyd更优
如题hdu 1704 Rank
我们将胜负关系想象成一个有向图,那么将有向边 A->B 定为A赢了B,再利用 floyd 处理传递闭包矩阵,那么最后统计无法连通的两个点的数量即可。注意,建模的是有向图,所以判断连通时,应当判断if(g[i][j]==0 && g[j][i]==0),不然就缺失了一部分关系
下为代码:

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x,y,sizeof(x))
#define debug(x) cout << #x << " = " << x << endl
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << endl
//#define int long long

using namespace std;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
typedef pair<ull,ull> pull;
typedef pair<double,double> pdd;
/*

*/
const int maxm=5e2+5,inf=0x3f3f3f3f,mod=998244353;
int n,m,ans;
vector<vector<int>> g;

void floyd(){
	for(int k=1;k<=n;++k){
		for(int i=1;i<=n;++i){
			if(g[i][k])
			for(int j=1;j<=n;++j){
				if(g[k][j])
					g[i][j]=1;
			}
		}
	}
	return ;
}

void solve(){
	cin>>n>>m;
	g=vector<vector<int>> (n+5,vector<int>(n+5,0));
	int a,b;
	for(int i=0;i<m;++i){
		cin>>a>>b;
		if(g[a][b]==0){
			g[a][b]=1;
		}
	}
	floyd();
	ans=0;
	for(int i=1;i<=n;++i){
		for(int j=i+1;j<=n;++j){
			if(g[i][j]==0 && g[j][i]==0) ++ans;
		}
	}
	cout<<ans<<'\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _=1;
	cin>>_;
	while(_--){
		solve();
	}
	return 0;
}
posted on 2023-06-25 21:20  Qiansui  阅读(22)  评论(0编辑  收藏  举报