单源最短路径

主要算法 Dijkstra 算法 & Bellman-Ford 算法

Dijkstra 算法

原理

以点为研究对象的贪心策略。

实现步骤

  1. 将图中的顶点分为已经找到最短路的点(下面称黑点)和尚未找到最短路的点(下面称白点)。
  2. 在所有白点中。找到距离起点 s 最近的点 cur 并染成黑色,vis[cur]=1
  3. 以 cur 为中转点,松弛 cur 的邻接点 y。 4. 重复步骤 22、步骤 33,直到所有点染成黑色。

时间复杂度 O(N^2)

优化策略

dis_idisi​ 会随着松弛(relax)操作更新,明显的,可以将其理解为动态求最小值,能用优先队列 priority_queue 优化。 用优先队列维护所有白点,并且是小根堆。

堆优化必用知识点——重载运算符(overload)

定义

是指将加减乘除等运算符修改为自定义的含义

标程
struct Node
{ 
	int y; 
	long double val; 
	bool operator<(const Node &b)const 
	{ 
		return val>b.val;
	} 
};

优化后Dijkstra时间复杂度

O(mlog n)

Dijkstra 与 BFS 的关系

BFS 是边权为 11 的 dijkstra。

注意事项

  1. 不能用于正负边权混杂的图。
  2. 正权不能跑最长路。
  3. 注意避免松弛操作溢出,#define int long long
  4. 多次调用 dijkstra 要重置 vis[] 和 dis[]

标程(弱化版)

#include<bits/stdc++.h> 
#define int long long 
using namespace std; 
const int N=1e4+5, M=1e5+5; 
int dis[N], n, m, s; bool vis[N]; 
struct node 
{ 
  int y, val; 
}; 
vector<node> nbr[N]; 
void dijkstra() 
{ 
  for(int i=1;i<=n;i++)
  { 
    dis[i]=2147483647;
  } 
  dis[s]=0;
  for(int i=1;i<=n;i++) 
  { 
    int mini=1e18, id; 
    for(int j=1;j<=n;j++) 
    { 
      if(mini>dis[j]&&!vis[j]) 
      { 
        id=j;
        mini=dis[j];
      } 
   } 
   vis[id]=1; 
   for(auto nxt:nbr[id]) 
   { 
     int y=nxt.y, val=nxt.val; 
     if(dis[id]+val<dis[y]) 
     { 
       dis[y]=dis[id]+val;
     } 
   } 
  } 
} 
signed main() 
{ 
  cin>>n>>m>>s; 
  for(int i=1;i<=m;i++) 
  { 
    int x, y, w; 
    cin>>x>>y>>w; 
    nbr[x].push_back({y,w}); 
  } 
  dis[s]=0; 
  dijkstra(); 
  for(int i=1;i<=n;i++) 
  { 
    cout<<dis[i]<<" "; 
  } 
}

标程(堆优化版)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n, m, s, dis[N];
bool vis[N];
struct node
{
	int y, val;
};
struct Node
{
	int y, val;
	bool operator<(const Node &b)const
	{
		return val>b.val;
	}
};
vector<node> nbr[N];

void dijkstra()
{
	for(int i=1;i<=n;i++)
	{
		dis[i]=2147483647;
	}
	dis[s]=0;
	priority_queue<Node> q;
	q.push((Node){s,0});
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		int cur=now.y;
		if(vis[cur])
		{
			continue;
		}
		vis[cur]=1;
		for(auto qq:nbr[cur])
		{
			int nxt=qq.y, w=qq.val;
			if(dis[nxt]>dis[cur]+w)
			{
				dis[nxt]=dis[cur]+w;
				q.push((Node){nxt,dis[nxt]});
			}
		}
	}
}

signed main()
{
	cin>>n>>m>>s;
	for(int i=1;i<=m;i++)
	{
		int x, y, w;
		cin>>x>>y>>w;
		nbr[x].push_back((node){y,w});
	}
	dijkstra();
	for(int i=1;i<=n;i++)
	{
		cout<<dis[i]<<" ";
	}
	return 0;
}

例题1——P1339

题目大意

有一个 n 个点 m 条边的无向图,请求出从 s 到 t 的最短路长度。

样例输入 #1
7 11 5 4 2 4 2 1 4 3 7 2 2 3 4 3 5 7 5 7 3 3 6 1 1 6 3 4 2 4 3 5 6 3 7 2 1
样例输出 #1
7
Floyd——80 pts 做法

就一模板。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, dis[2505][2505], t, s;
signed main()
{
	cin>>n>>m>>s>>t;
	for(int i=1;i<=2500;i++)
	{
	    for(int j=1;j<=2500;j++)
	    {
	        dis[i][j]=1e9;
	    }
	}
	for(int i=1;i<=m;i++)
	{
		int x, y, w;
		cin>>x>>y>>w;
		dis[x][y]=dis[y][x]=w;
	}
	for(int i=1;i<=n;i++)
	{
		dis[i][i]=0;
	}
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
			    if(dis[i][k]==1000000000||dis[k][j]==1000000000)
			    {
			        continue;
			    }
			    dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
			}
		}
	}
	cout<<dis[s][t];
}
dijkstra——100 pts 做法

也就一模板,注意是无向图。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, m, s, t;
struct node
{
	int y, val;
};
vector<node> nbr[2505];
int dis[2505];
bool vis[2505];
struct Node
{
	int y, val;
	bool operator<(const Node &b)const
	{
		return val>b.val;
	} 
};
void dijkstra()
{
	memset(vis,0,sizeof vis);
	priority_queue<Node> q;
	for(int i=1;i<=n;i++)
	{
		dis[i]=2147483647;
	}
	q.push((Node){s,0});
	dis[s]=0;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		int cur=now.y;
		if(vis[cur])
		{
			continue;
		}
		vis[cur]=1;
		for(auto nxt:nbr[cur])
		{
			int y=nxt.y, val=nxt.val;
			if(dis[cur]+val<dis[y])
			{
				dis[y]=dis[cur]+val;
				q.push((Node){y,dis[y]});
			}
		}
	} 
}
signed main()
{
	cin>>n>>m>>s>>t;
	for(int i=1;i<=m;i++)
	{
		int x, y, w;
		cin>>x>>y>>w;
		nbr[x].push_back((node){y,w});
		nbr[y].push_back((node){x,w});
	}
	dis[s]=0;
	dijkstra();
	cout<<dis[t];
}

例题2——P1629

题目大意

有一个邮递员要送东西,邮局在节点 1。他总共要送 n−1 样东西,其目的地分别是节点 22 到节点 n。由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m 条道路。这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。求送完这 n-1 样东西并且最终回到邮局最少需要的时间。

样例输入 #1
5 10 2 3 5 1 5 5 3 5 6 1 2 8 1 3 8 5 3 4 4 1 8 4 5 3 3 5 6 5 4 2
样例输出 #1
83
分析

很明显,求起点到各个点的最短路,一遍是求起点到各个点最短路和,另一遍求各个点返回起点最短路和,跑两遍 Dijkstra。 建反图,两次 Dijkstra 之间要清空。

代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5, M=1e5+5;
int dis[N], n, m, s;
bool vis[N];
struct node
{
	int y, val;
};
struct Node
{
	int y, val;
	bool operator<(const Node &b)const
	{
		return val>b.val;
	}
};
vector<node> nbr[N];
void dijkstra()
{
	
	memset(vis,0,sizeof vis);
	priority_queue<Node> q;
	
	for(int i=1;i<=n*2;i++)
	{
		dis[i]=2147483647;
	}
	q.push((Node){s,0});
	dis[s]=0;
	while(!q.empty())
	{
		Node now=q.top();
		q.pop();
		int cur=now.y;
		if(vis[cur])
		{
			continue;
		}
		vis[cur]=1;
		for(auto nxt:nbr[cur])
		{
			int y=nxt.y, val=nxt.val;
			if(dis[cur]+val<dis[y])
			{
				dis[y]=dis[cur]+val;
				q.push((Node){y,dis[y]});
			}
		}
	}
	
}
int t;
signed main(void)
{
	int cnt=0;
	cin>>n>>m;
	s=1;
	for(int i=1;i<=m;i++)
	{
		int x, y, w;
		cin>>x>>y>>w;
		nbr[x].push_back({y,w});
		nbr[y+n].push_back({x+n,w});
	}
	dis[s]=0;
	dijkstra();
	for(int i=2;i<=n;i++)
	{
		cnt+=dis[i];
	}
	s=n+1;
	dis[s]=0;
	dijkstra();
	for(int i=2+n;i<=n+n;i++)
	{
		cnt+=dis[i];
	}
	cout<<cnt;
}

拓展提升 P1346 P1576

P1576关键代码——Dijkstra

memset(vis,0,sizeof vis);
	dis[s]=100.0;
	q.push(s);
	vis[s]=1;
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		vis[cur]=0;
		for(auto qq:nbr[cur])
		{
			int nxt=qq.y;
			double w=qq.w;
			if(dis[cur]/w<dis[nxt])
			{
				dis[nxt]=dis[cur]/w;
				if(!vis[nxt])
				{
					vis[nxt]=1;
					q.push(nxt);
				}
			}
		}
	}

Bellman-Ford 算法

概述

Bellman-Ford 算法是由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的,求解单源最短路径问题的一种算法。

有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。

它的原理是对图进行 m-1 次松弛操作,得到所有可能的最短路径。

其优于 Dijkstra 算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达 O(n×m)。但算法可以进行若干种优化,提高了效率。

实现

双重循环枚举,和暴力差不多。

#include<bits/stdc++.h>
#define int long long
using namespace std;
struct node
{
    int x, y, w;
}e[2000005];
int n, m, s;
int dis[200005];
void bellman_ford()
{
    for(int i=1;i<=n;i++)
    {
        dis[i]=2147483647;
    }
    dis[s]=0;
    for(int t=1;t<n;t++)
    {
        for(int i=1;i<=m;i++)
        {
            if(dis[e[i].x]+e[i].w<dis[e[i].y])
            {
                dis[e[i].y]=dis[e[i].x]+e[i].w;
            }
        }
    }
}
signed main()
{
    cin>>n>>m>>s;
    for(int i=1;i<=m;i++)
    {
        cin>>e[i].x>>e[i].y>>e[i].w;
    }
    bellman_ford();
    for(int i=1;i<=n;i++)
    {
        cout<<dis[i]<<" ";
    }
}

SPFA 算法

死了的算法。

理论上对 Bellman-Ford 算法进行优化,但这种优化被证实可卡掉的。

用队列进行松弛。

除了队列优化(SPFA)之外,Bellman–Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
  • LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
  • SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。
  • D´Esopo–Pape 算法:将普通队列换成双端队列,如果一个节点之前没有入队,则将其插入队尾,否则插入队首。

上述“其他优化”来自 OI-WIKI

但一句话总结,都会被卡。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int t, n, m;
struct node
{
	int y, w;
};
vector<node> nbr[N];
int cnt[N], dis[N];
bool vis[N];
void spfa()
{
	memset(cnt,0,sizeof cnt);
	memset(vis,0,sizeof vis);
	memset(dis,0x3f,sizeof dis);
	queue<int> q;
	q.push(1);
	vis[1]=1;
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		vis[cur]=0;
		for(auto qq:nbr[cur])
		{
			int nxt=qq.y, val=qq.w;
			if(dis[cur]+val<dis[nxt])
			{
				dis[nxt]=dis[cur]+val;
				cnt[nxt]=cnt[cur]+1;
				if(cnt[nxt]>n-1)
				{
					cout<<"YES\n";
					return ;
				}
				q.push(nxt);
			}
		}
	}
	cout<<"NO\n";
	return ;
}
signed main()
{
	cin>>t;
	while(t--)
	{
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		{
			nbr[i].clear();
		} 
		for(int i=1;i<=m;i++)
		{
			int x, y, w;
			cin>>x>>y>>w;
			if(w>=0)
			{
				nbr[y].push_back((node){x,w});
			}
			nbr[x].push_back((node){y,w});
		}
		spfa();
	}
}

我们用队列来维护哪些结点可能会引起松弛操作,就能避免访问不必要的边。

这个算法已经死了就不多赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值