最短路算法
最短路问题
给定起点 s 和终点 t ,在所有能连接 s 和 t 的路径中,寻找边的权值之和最小的路径,就是最短路径问题
相关资料:
https://oi-wiki.org/graph/shortest-path/
单源最短路-迪杰斯特拉算法
用于计算一个节点到其他所有节点的最短路径
Dijkstra 算法是贪心算法, 是一种求解非负权图上单源最短路径的算法。
基本思想是:设置一个顶点的集合S,并不断地扩充这个集合,当且仅当从源点到某个点的路径已求出时它才属于集合S。开始时S中仅有源点,调整S 集合之外的点的最短路径长度, 并从中找到当前最短路径点,将其加入到集合S,直到所有的点都在S中。
仅对非负权边图有效
对于 n 个顶点,m 条边的非负权图
- 若为稀疏图,点和边的数量差不多,则利用优先队列优化找最近距离点
- 若为稠密图,点少于边,则暴力\(O(n^2)\)找最近距离更加有效
注意一点:如果使用优先队列进行优化,记得使用最小堆!不然优先队列默认是最大堆!!!
模板题
注意题目所给图为无向图
代码:
https://loj.ac/s/1803867 与上同
https://loj.ac/s/1803868 受到启发,改了一小点,此处无需给head数组赋初值
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
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;
}
//>>>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;
}
//>>>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;
}