【模板】最短路

图论总结

最短路

最短路算法 Floyd Bellman–Ford Dijkstra Johnson
最短路类型 每对结点之间的最短路 单源最短路 单源最短路 每对结点之间的最短路
作用于 任意图 任意图 非负权图 任意图
能否检测负环? 不能
时间复杂度 \(O(N^3)\) \(O(NM)\) \(O(M\log M)\) \(O(NM\log M)\)

注:表中的 Dijkstra 算法在计算复杂度时均用 priority_queue 实现。

Floyd

适用范围:无论有向图还是无向图,无论是否有负边。(正常运行)不能有负环。

定义数组f[k][x][y],表示只允许经过1-k的节点,x到y的最短路径长度。(x,y不一定要在1-k中)
本质是dp。第一维可压。
初始化:边权,或0(x=y),或inf(不连接)。

int e[maxn][maxn], f[maxn][maxn];

void floyd()
{
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		{
			f[i][j] = (e[i][j] == -1 ? inf : e[i][j]);
			if (i == j)
				f[i][j] = 0;
		}
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}

Bellman–Ford

适用范围:允许有负边,可以判断负环。
松弛操作:dis(v) = min(dis(v), dis(u) + w(u, v)) 。

关于负环:
若存在最短路(没有负环),由于每一次松弛会使得最短路边数至少 +1,而最短路的边数最多为 n-1,因此整个算法最多执行 n-1 轮松弛操作。若第n轮还能继续,则存在负环。
需要注意,从S出发能跑出最短路,不代表一定没有负环(如有向图,或不属于同一个分量的情况)。正确做法是建立一个超级原点S',从它向每个点连一条边权为0的边,然后从这个点开始跑。(不过如果只是找S的最短路也无所谓了)

只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。所以有了SPFA,即队列优化。
SPFA会被卡,所以一般情况用Dijkstra。

bool SPFA()//返回true代表有最短路,否则有负环
{
	memset(dis,0x3f,sizeof(dis));//由于memset的妙妙特性,dis会变成0x3f3f3f3f
	queue<int> qu;
	dis[s]=0,inque[s]=1;
	qu.push(s);
	while (!qu.empty())
	{
		int u=qu.front();
		qu.pop();inque[u]=0;
		for(edge ed:e[u])//相当于for(int i=0;i<e[u].size();i++)edge ed=e[u][i]
		{
			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,有负环
				if(!inque[v]) 
					qu.push(v), inque[v]=1;
			}
		}
	}
	return true;
}

Dijkstra

适用范围:非负权图(可以有0)
思路:将结点分成两个集合:已确定最短路长度的点集(记为 S 集合)的和未确定最短路长度的点集(记为 T 集合)。一开始所有的点都属于 T 集合。每次从T中选取一个最短路长度最小的结点,移到 S 集合中,并进行松弛操作。
本质为贪心。

struct edge{
	int u,v,w;
};
struct node{
	int u,dis;
	bool operator > (const node& b) const {return dis>b.dis;};//用于greater;const不能少
};
vector<edge> e[maxn];
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node>> pq;

void dijkstra()
{
	memset(dis, 0x3f, (n + 1) * sizeof(int));
	memset(vis, 0, (n + 1) * sizeof(int));
	dis[s]=0;
	pq.push({s,0});
	while (!pq.empty())
	{
		int u=pq.top().u;
		pq.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(edge ed:e[u])
		{
			int v=ed.v,w=ed.w;
			if(dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				pq.push({v,dis[v]});
			}
		}
	}
}

Johnson

适用范围:全源最短路,可以有负边,不能有负环。

跑n遍Dijkstra实现全源最短路的复杂度是\(O(nm\log m)\),在稀疏图上比Floyed更优。但是Dijkstra不能解决负边。因此需要将值全部调整为正的。

新建虚拟节点0,从0向所有节点连一条权为0的边。(注:即使是无向图,也只用添加单向的边)接下来从0开始求最短路,记为h[i]。如果一条边从u到v,权为w,则新的权值为为 \(w+h_u-h_v\)。然后从每个节点跑一次Dijkstra。

模板:洛谷P5905

//公共部分
const int maxn = 3e3+10;
int n,m;

struct edge{
	int u,v,w;
};
vector<edge> e[maxn];

//spfa变量区
int h[maxn],cnt[maxn],inque[maxn];
const int inf = 1e9;

bool SPFA(int s)//返回true代表有最短路,否则有负环
{
	for(int i=1;i<=n;i++)
		h[i]=inf;
	queue<int> qu;
	h[s]=0,inque[s]=1;
	qu.push(s);
	while (!qu.empty())
	{
		int u=qu.front();
		qu.pop();inque[u]=0;
		for(edge ed:e[u])//相当于for(int i=0;i<e[u].size();i++)edge ed=e[u][i]
		{
			int v=ed.v,w=ed.w;
			if(h[v]>h[u]+w)
			{
				h[v]=h[u]+w;
				cnt[v]=cnt[u]+1;
				if(cnt[v]>=n+1)//注意在Johnson中是n+1,因为有0号原点
					return false;//大于等于n+1,有负环
				if(!inque[v]) 
					qu.push(v), inque[v]=1;
			}
		}
	}
	return true;
}

//dijkstra变量区
struct node{
	int u,dis;
	bool operator > (const node& b) const {return dis>b.dis;};//用于greater;const都不能少
};
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node>> pq;

void dijkstra(int s)
{
	for(int i=1;i<=n;i++)
		dis[i]=inf;
	memset(vis, 0, (n + 1) * sizeof(int));
	dis[s]=0;
	pq.push({s,0});
	while (!pq.empty())
	{
		int u=pq.top().u;
		pq.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(edge ed:e[u])
		{
			int v=ed.v,w=ed.w;
			if(dis[v]>dis[u]+w)
			{
				dis[v]=dis[u]+w;
				pq.push({v,dis[v]});
			}
		}
	}
}

void johnson()
{
	for(int i=1;i<=n;i++)
		e[0].push_back({0,i,0});
	bool y=SPFA(0);
	if(!y)
	{
		cout<<-1<<endl;
		return;
	}
	for(int i=1;i<=n;i++)
		for(edge& ed:e[i])
			ed.w=ed.w+h[ed.u]-h[ed.v];//重新赋值
	for(int i=1;i<=n;i++)
	{
		dijkstra(i);
		int ans=0;
		for(int j=1;j<=n;j++)//本题要求是这么处理,其他题看情况
		{
			if(dis[j]==inf)
				ans+=j*inf;//这里需要注意,最后不要用dis[j]+h[j]-h[i]对inf算
			else
				ans+=j*(dis[j]+h[j]-h[i]);//别忘记了
		}
		cout<<ans<<endl;
	}
}
posted @ 2025-04-21 18:44  Astral_Plane  阅读(20)  评论(0)    收藏  举报