首页 > 编程语言 >最短路与生成树算法

最短路与生成树算法

时间:2023-06-02 11:45:03浏览次数:48  
标签:int text 短路 ans 生成 算法 qquad gets dis

写在前面

最短路部分的代码还是 3 月的,奇丑无比,大家见谅……


最短路

单源最短路径

首先我们介绍一些基本概念。

由于是单源最短路,我们定义一个起点 \(s\),\(dis_u\) 表示起点 \(s\) 到节点 \(u\) 的最短路长度。

一般来讲,对于一条为 \(w\) 的边 \(u \to v\),如果目前的最短路是正确的,都应该满足:

\[dis_u+w \geq dis_v \]

我们称之为 三角形不等式

对于不满足三角形不等式的,我们就要更新最短路了:

一条边权为 \(w\) 的边 \(u \to v\),如果满足 \(dis_u+w<dis_v\),即可用 \(dis_u+w\) 更新 \(dis_v\)。

这个更新的过程叫做 松弛

松弛是所有最短路算法的基本操作,我们这里讲的是单源,其实多源也是一样的道理。

Dijkstra

首先是我们的老朋友迪科斯彻。

将有项带权图 \(G=(V,E)\) 的点集 \(V\) 分为两个集合:\(S\) 和 \(T\),\(S\) 中的点已确定最短路径长度,(即 \(dis_u\) 已更新。起初, \(S\) 中仅包含源点 \(s\),除 \(dis_s=0\) 外 \(dis\) 初值皆为 \(+\infty\)。)\(T\) 中的点没有确定。

迪科斯彻采用了 贪心 的思想,在 \(S\) 中选择 \(dis_u\) 最小的节点 \(u\),对 \(u\) 的所有出边进行松弛。

可以证明,由于贪心,每个节点的 \(dis\) 只会更新一次,所以可以对已经松弛所有出边的节点 \(u\) 打标,这个松弛操作 只进行一次 即可。

当然了,最原始的 Dijkstra 时间复杂度还是太假。由于我们只选取最短的一条特殊路径进行松弛,其实可以采用伟大的标准模板库 STL 中的 priority_queue 解决这个问题。

解决之后,我们最坏对 \(m\) 条边进行松弛,优先队列单次操作复杂度 \(O(\log n)\),所以总复杂度 \(O(m \log n)\),非常优秀。

接下来是模板题 单源最短路径(标准版) 的 \(Code\):

#include<bits/stdc++.h>
const int N=1e5+5;
struct Edge { int to,w;
	bool operator < (const Edge &a) const {return w>a.w;}
};
std::vector <Edge> E[N];//使用结构体和vector存边
int n,m,s,dis[N];
bool flag[N];
void Dijkstra(int start)
{
	memset(dis,0x3f,sizeof dis);
	std::priority_queue <Edge> q;
	dis[start]=0;
	q.push({start,0});
	while(!q.empty()){
		int u=q.top().to;
		q.pop();
		if(flag[u])	continue;
		/*每个点仅一次作为媒介节点参与松弛。*/
		flag[u]=1;
		for(auto v:E[u]){
			if(dis[u]+v.w<dis[v.to]){
				dis[v.to]=dis[u]+v.w;//松弛
				q.push({v.to,dis[v.to]});//入集合S
			}
		}
	}
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0);std::cout.tie(0);
	std::cin>>n>>m>>s;
	for(int i=1;i<=m;++i){
		int u,v,w;
		std::cin>>u>>v>>w;
		E[u].push_back({v,w});
	}
	Dijkstra(s);
	for(int i=1;i<=n;++i)
		std::cout<<dis[i]<<" ";
	return 0;
}

但是呢,Dijkstra 不能用于 负环。因为在 Dijkstra 中,每一个顶点作为媒介节点参与松弛操作只有一次,所以得出的结果其实是松弛一次的结果,且无法进行判断其是否是负环。

同时,存在 负边权 的图也 不可 使用 Dijkstra。

Bellman-Ford

其实是一个非常朴素的想法,朴素到其复杂度为 \(O(VE)\),本质就是对每一条边都尝试松弛。因为其复杂度过于高,实际用途已经基本废了。所以现在主要介绍的是其优化后的算法,SPFA。

SPFA

Shortest Path Faster Algorithm, AKA SPFA

实际上,这个名字只在大陆存在。因为他实际上叫做 队列优化的 Bellman-Ford 算法。顾名思义,使用队列来优化 Bellman-Ford,本质思想还是朴素的对每个边进行松弛,而且——

\[\text{关于 SPFA, }\textbf{他死了。} \]

这个帖子 足见杀他的方法已经发展得很完备了。)

所以直接上 模板题 代码吧。

\(Code\):

#include<bits/stdc++.h>
const int N=1e4+5;
int n,m,s,dis[N];
bool vis[N];
struct Edge	{int to,w;};
std::vector<Edge> E[N];
void SPFA(int start){
	std::queue<int> q;
	for(int i=1;i<=n;++i)
		dis[i]=INT_MAX;
	vis[start]=1,dis[start]=0;
	q.push(start);
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=0;
		for(auto v:E[u]){
			if(dis[u]+v.w<dis[v.to]){
				dis[v.to]=dis[u]+v.w;
				if(!vis[v.to]){
					vis[v.to]=1;
					q.push(v.to);
				}
			}
		}
	}
}
int main(){
	std::ios::sync_with_stdio(0);
	std::cin.tie(0);std::cout.tie(0);
	std::cin>>n>>m>>s;
	for(int i=1;i<=m;++i){
		int u,v,w;
		std::cin>>u>>v>>w;
		E[u].push_back({v,w});
	}
	SPFA(s);
	for(int i=1;i<=n;++i)
		std::cout<<dis[i]<<" ";
	return 0;
}

SPFA 的复杂度一般是 \(O(km)\) 的,但是在一些特殊图(如网格图和链套菊花)中会退化到 \(O(nm)\),所以,慎用

最后的用武之地 - 判断负环

在 SPFA 中,一个节点最多被松弛 \(n\) 次。所以,我们可以记录每个节点 \(u\) 被松弛的次数 \(sum_u\),如果出现 \(sum_n>n\),就可以判定出现负环了。


多源最短路径 - Floyd

OK,现在我们来说一下最后的,多源最短路径。解决他的算法是 Floyd。

其实这个 Floyd 就是一个动态规划,使用邻接矩阵 \(dis(i,j)\) 表示从 \(i\) 到 \(j\) 的最短路径长,枚举每一个节点 \(k\),判断其是否满足三角形不等式,不满足就 \(dis(i,j) \gets dis(i,k)+dis(k,j)\)。

\(Code:\)

for(int k=1;k<=n;++k){
	for(int i=1;i<=n;++i){
    	for(int j=1;j<=n;++j){
        	dis[i][j]=std::min(dis[i][j],dis[i][k]+dis[k][j]);
		}
	}
}

Floyd 是敲着最简单的,但是 \(O(n^3)\) 的复杂度,所以小一点的图哪怕单源也可以凑合用,要是图很大就慎重吧。


生成树

本部分旨在速通,复习用,看细节的朋友可以看 catandcode 的博客。

最小生成树 - MST

性质

就是:

对于一个无向连通图 \(G=(V,E)\),点集 $U \subsetneq V $ 和 边集 \(A=\{ u \leftrightarrow v|u \in U,v \in (V-U) \} \subsetneq E\),有无向边 \(u \leftrightarrow v \in A\) 且在 \(A\) 中边权 \(w_{u,v}\) 最小,则这条边必然在该图 \(G\) 的一棵最小生成树中。

Prim

将无向连通图 \(G=(V,E)\) 分为两个集合:已处理 \(A\) 和未处理 \(B\)。处理的过程如下:

  1. 将一个节点 \(u\) 放入集合 \(A\);
  2. 在边集 \(C=\{ i \leftrightarrow j | i \in A,j \in B\}\subset E\) 寻找最小的一条边,将这条边纳入最小生成树;
  3. 重复第 2 步,直至 \(B= \varnothing\)。

Prim 更适合 稠密图

\(\text{Code - with Prim by Adjacency List, } 1.19 s \text{ without O2}\)

#include<bits/stdc++.h>
const int MAXN=5005,inf=0x3f3f3f3f;
int n,m,sp[MAXN],dis[MAXN][MAXN];
bool flag[MAXN];
int prim() {
    int ans=0,tot=0; sp[0]=inf,flag[1]=1;
    for(int i=2;i<=n;++i) sp[i]=dis[1][i];
    for(int i=1;i<n;++i) {
    	int tmp=0;
        for(int v=1;v<=n;++v) if(!flag[v] && sp[v]<sp[tmp]) tmp=v;
        if(!tmp) break;
        ans+=sp[tmp],flag[tmp]=1,++tot;
        for(int l=1;l<=n;++l) if(!flag[l] && dis[tmp][l]<sp[l]) sp[l]=dis[tmp][l];
    }
    return tot==n-1?ans:-1;
}
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr); std::cout.tie(nullptr);
    std::cin>>n>>m; memset(dis,0x3f,sizeof dis);
    for(int i=1,u,v,w;i<=m;++i) { std::cin>>u>>v>>w; dis[u][v]=dis[v][u]=std::min(dis[u][v],w); }
    int ans=prim();
	if(~ans) std::cout<<ans<<'\n';
	else std::cout<<"orz\n";
	return 0;
}

下面是前向星 prim. 与邻接矩阵不同的是,邻接矩阵需要判断重边,\(sp\) 就可以直接赋值;而前向星不需要判断重边,所以 \(sp\) 需要取所有边中的最小值。

\(\text{Code - with Prim by Forward Star, } 474 ms \text{ without O2}\)

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=5005,M=2e5+5,inf=0x3f3f3f3f;
int cnt,head[N];
struct edge { int to,next,w; } E[M<<1];
void add(int u,int v,int w) { E[++cnt].to=v,E[cnt].w=w,E[cnt].next=head[u],head[u]=cnt; }
int n,m,sp[N];
bool flag[N];
int prim() {
	memset(sp,0x3f,sizeof sp);
	int ans=0,tot=0; flag[1]=1;
	forE(1) sp[v]=std::min(sp[v],E[p].w);
	for(int i=1;i<=n;++i) {
		int tmp=0;
		for(int v=1;v<=n;++v) if(!flag[v] && sp[v]<sp[tmp]) tmp=v;
		if(sp[tmp]==inf) break;
		ans+=sp[tmp],flag[tmp]=1,++tot;
		forE(tmp) sp[v]=std::min(sp[v],E[p].w);
	}
	return tot==n-1?ans:-1;
}
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr); std::cout.tie(nullptr);
	std::cin>>n>>m;
	for(int i=1,u,v,w;i<=m;++i) { std::cin>>u>>v>>w; add(u,v,w),add(v,u,w); }
	int ans=prim();
	if(~ans) std::cout<<ans<<'\n';
	else std::cout<<"orz\n";
	return 0;
}

Kruskal

基于贪心的思想,使用并查集维护状态(一个集合一棵树),将所有边从小到大排序后遍历,如果两个点不在同一棵树上,则将该边纳入 MST,合并这条边连通的两个端点的集合。

Kruskal 更适合 稀疏图

\(\text{Code - with Kruskal by Forward Star and Disjoint Set Union, } 230 ms \text{ without O2}\)

#include<bits/stdc++.h>
#define ll long long
#define ld long double
const int N=5005,M=2e5+5;
struct edge { int from,to,w; } E[M];
void add(int u,int v,int w,int ord) { E[ord].from=u,E[ord].to=v,E[ord].w=w; }
int n,m,fa[N];
void init() { for(int i=1;i<=n;++i) fa[i]=i; }
int get(int x) { return fa[x]==x?x:fa[x]=get(fa[x]); }
void merge(int x,int y) { fa[get(y)]=get(x); }
int kruskal() {
	init();
	std::sort(E+1,E+m+1,[](const edge &a,const edge &b){ return a.w<b.w; });
	int ans=0,tot=0; edge *it=E;
	while(++it<E+m+1) {
		if(get(it->from)==get(it->to)) continue;
		ans+=it->w,merge(it->from,it->to),tot++;
		if(tot==n-1) break;
	}
	return tot==n-1?ans:-1;
}
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr); std::cout.tie(nullptr);
	std::cin>>n>>m;
	for(int i=1,u,v,w;i<=m;++i) { std::cin>>u>>v>>w; add(u,v,w,i); }
	int ans=kruskal();
	if(~ans) std::cout<<ans<<'\n';
	else std::cout<<"orz\n";
	return 0;
}

最大生成树

有什么好说的呢…算法相同,其实就只把排序/比较大小反过来罢了。

大概就是这样。

次小生成树

分为 非严格次小生成树严格最小生成树。差别其实就是是否有边权和 严格大于 MST 边权和。

简单来说,在找到 MST 后枚举未在 \(E_{MST}\) 中的边 \(u \to v\),然后断开 \(u\) 到 \(v\) 的路径上的最长边,将当前边加入边集,寻找最小的边权和即可。对于严格最小生成树,考虑到最长边边权可能与当前边权相同,还要同时记录 次大值

严格最小生成树的伪代码如下:

\[\begin{array}{ll} 1 & \textbf{Input. } \text{The number of vertexs and edges of the undigraph } G, \\ & \text{the edges of the graph } e, \text{which element in } e \text{ is } (u,v,w) \\ & \text{denoting that there is an edge between } u \text{ and } v \text{ weighted } w . \\ 2 & \textbf{Output. } \text{The sum of the weights of the edges in the set of edges } E_{S}.\\ & \text{Or formally, } \sum_{e \in E_{S}} w_e.\\ 3 & \textbf{Method. }\\ 4 & \text{initialize the disjoint set union} \\ 5 & mstlen \gets 0,E_M \gets \varnothing \\ 6 & \text{sort } e \text{ into nondecreasing order by weight } w \\ 7 & \textbf{for } \text{each } (u,v,w) \text{ ordered } i \text{ in the sorted } e\\ 8 & \qquad \textbf{if } u \text{ and } v \text{ are not connected in the disjoint set union } \\ 9 & \qquad \qquad mstlen \gets mstlen + w\\ 10 & \qquad \qquad \text{merge } u \text{ and } v \text{ in the disjoint set union}\\ 11 & \qquad \qquad E_M \gets E_M \bigcup \{(u,v,w)\} \\ 12 & dfs(u \gets 1,fa \gets 0) \text{ to initialize the binary-lifting funtions} \\ 13 & ans \gets \infty \\ 14 & \textbf{for } \text{each } (u,v,w) \text{ which is not in } E_M \text{ and satisfies the limit } u \neq v \\ 15 & \qquad lca \gets \operatorname{LCA}(u,v),tmp_1 \gets - \infty, tmp_2 \gets - \infty \\ 16 & \qquad tmp_1 \gets \text{the maximum weight on the path from } lca \text{ to } u \\ 17 & \qquad \textbf{if } tmp_1 = w \\ 18 & \qquad \qquad tmp_1 \gets \text{the sub-maximum weight on the path from } lca \text{ to } u \\ 19 & \qquad tmp_2 \gets \text{the maximum weight on the path from } lca \text{ to } v \\ 20 & \qquad \textbf{if } tmp_2 = w \\ 21 & \qquad \qquad tmp_2 \gets \text{the sub-maximum weight on the path from } lca \text{ to } v \\ 22 & \qquad \textbf{if } tmp_1 = tmp_2 = - \infty \\ 23 & \qquad \qquad \textbf{continue} \\ 24 & \qquad ans \gets \min \left( ans, mstlen- \max(tmp_1,tmp_2)+w \right) \\ 25 & \textbf{return }ans \end{array} \]

放一段优美的 \(code\):

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define forE(u) for(int p=head[u],v=E[p].to;p;p=E[p].next,v=E[p].to)
const int N=1e5+5,M=3e5+5;
const ll inf=1e18+9;
int n,m;
// set of edges module
bool used[M];
struct pr { int from,to; ll w; } prE[M];
inline void add(int u,int v,ll w,int id) { prE[id].from=u,prE[id].to=v,prE[id].w=w; }
int cnt,head[N];
struct edge { int to,next; ll w; } E[N<<1];
inline void addedge(int u,int v,ll w) { E[++cnt].to=v,E[cnt].w=w,E[cnt].next=head[u],head[u]=cnt; }
// DSU module
int fa[N];
void init() { for(int i=1;i<=n;++i) fa[i]=i; }
int get(int x) { return fa[x]==x?x:fa[x]=get(fa[x]); }
void merge(int x,int y) { fa[get(y)]=get(x); }
// kruskal module
ll kruskal() {
	init();
	std::sort(prE+1,prE+m+1,[](const pr &a,const pr &b){ return a.w<b.w; });
	int tot=0; ll ans=0; pr *it=prE;
	while(tot<n-1) {
		it++; if(it==prE+m+1) break;
		if(get(it->from)==get(it->to)) continue;
		ans+=it->w,used[it-prE]=1,merge(it->from,it->to),tot++;
		addedge(it->from,it->to,it->w),addedge(it->to,it->from,it->w);
	}
	return ans;
}
// MST module
int dep[N],anc[22][N];
ll mx[22][N],sec[22][N];
void dfs(int u,int f) {
	dep[u]=dep[f]+1,anc[0][u]=f,sec[0][u]=-inf;
	for(int i=1;(1<<i)<=dep[u];++i) {
		anc[i][u]=anc[i-1][anc[i-1][u]];
		ll tmp[]={mx[i-1][u],mx[i-1][anc[i-1][u]],sec[i-1][u],sec[i-1][anc[i-1][u]]};
		std::sort(tmp,tmp+4);
		mx[i][u]=tmp[3];
		int ptr=2;
		while(~ptr && tmp[ptr]==tmp[3]) ptr--;
		sec[i][u]=~ptr?tmp[ptr]:-inf;
	}
	forE(u) if(v!=f) mx[0][v]=E[p].w,dfs(v,u);
}
int LCA(int u,int v) {
	if(dep[v]>dep[u]) std::swap(u,v);
	int d=dep[u]-dep[v];
	for(int i=20;~i;--i) if(d&(1<<i)) u=anc[i][u];
	if(u==v) return u;
	for(int i=20;~i;--i) if(anc[i][u]!=anc[i][v]) u=anc[i][u],v=anc[i][v];
	return anc[0][u];
}
ll query(int u,int v,ll w) {
	ll ans=-inf;
	if(dep[v]>dep[u]) std::swap(u,v);
	int d=dep[u]-dep[v];
	for(int i=20;~i;--i) if(d&(1<<i)) {
		if(mx[i][u]==w) ans=std::max(ans,sec[i][u]);
		else ans=std::max(ans,mx[i][u]);
		u=anc[i][u];
	}
	return ans;
}
// beautiful main program
int main() {
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr); std::cout.tie(nullptr);
	std::cin>>n>>m;
	for(int i=1,u,v,w;i<=m;++i) { std::cin>>u>>v>>w; add(u,v,w,i); }
	ll mstlen=kruskal(),ans=inf; dfs(1,0);
	for(int i=1;i<=m;++i) {
		if(used[i] || prE[i].from==prE[i].to) continue;
		int lca=LCA(prE[i].from,prE[i].to);
		ll tmp1=query(prE[i].from,lca,prE[i].w),tmp2=query(prE[i].to,lca,prE[i].w);
		if(tmp1==-inf && tmp2==-inf) continue;
		ans=std::min(ans,mstlen-std::max(tmp1,tmp2)+prE[i].w);
	}
	std::cout<<ans<<'\n';
	return 0;
}

标签:int,text,短路,ans,生成,算法,qquad,gets,dis
From: https://www.cnblogs.com/michaelwong007/p/shortest-path-and-spanning-tree-algorithms.html

相关文章

  • 直播系统开发知识,软件宣传码的生成
    直播的火爆促进了直播源码开发平台的火爆,许许多多的公司加入了开发直播平台的队列中来。当一个公司去开发完直播平台的时候,宣传就成了这个公司的重中之重,在宣传中,让对方下载其中的重点之一,下载有许许多多的方式,不知道大家有没有见过这样的事情,在奶茶店或是商场中,都有由他们店里自己......
  • 代码随想录算法训练营第二十三天|669. 修剪二叉搜索树
    [参考链接]669.修剪二叉搜索树 [代码]1#Definitionforabinarytreenode.2#classTreeNode(object):3#def__init__(self,val=0,left=None,right=None):4#self.val=val5#self.left=left6#self.right=right......
  • python算法学习——第1天
    目录1、3,5,7的倍数判定2、鸡兔同笼3、计算有n个字符串中最长的字符串长度4、输出10个不重复的英文字母5、统计一段文字的单词个数并按字母顺序排序输出6、字典合并7、最大公约数&最小公倍数8、输出全排列9、输出<=n的全部回文数10、重复元素判定1、3,5,7的倍数判定num=int(inp......
  • STL algorithm算法
    Functionsin<algorithm>Non-modifyingsequenceoperations:all_ofTestconditiononallelementsinrange(functiontemplate)any_ofTestifanyelementinrangefulfillscondition(functiontemplate)none_ofTestifnoelementsfulfillconditi......
  • 算法学习day39动态规划part02-62、63
    packageLeetCode.DPpart02;/***62.不同路径*一个机器人位于一个mxn网格的左上角(起始点在下图中标记为“Start”)。*机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。*问总共有多少条不同的路径?*示例:*输入......
  • 算法学习day41动态规划part03-343、96
    packageLeetCode.DPpart03;/***343.整数拆分*给定一个正整数n,将其拆分为k个正整数的和(k>=2),并使这些整数的乘积最大化。*返回你可以获得的最大乘积。*示例:*输入:n=2*输出:1*解释:2=1+1,1×1=1。**/publicclassIntegerBre......
  • 算法题——数组(一)
    1、两数之和给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。/*建一个hash表,key存放值,value存放下标遍历数组,如果表里存在target-nums[i],则返回下标不存在则把当前的数存到hash表*/cl......
  • 二分法 三元表达式 生成式 匿名函数 内置函数
    目录二分法三元表达式生成式列表生成式字典生成式集合生成式元组生成式(生成器)匿名函数内置函数二分法二分法思路1.二分法的使用前提条件:列表中得数字必须要有序2.将对象整除2分成两部分3.将目标数值与分割后的对象做比较来确定目标数值在哪一部分4.继续重复这两个步骤直至......
  • 算法之二分法、三元表达式、列表生成式、字典生成式(了解)、匿名函数、常见的内置函数
    算法之二分法二分概念二分算法,又称折半查找,即在一个单调有序的集合中查找一个解。每次分为左右两部分,判断解在哪个部分中并调整上下界,直到找到目标元素,每次二分后都将舍弃一半的查找空间。定义and实现:算法就是解决问题的高效办法常见的算法:二分法算法还可以锻炼我们的......
  • 文心一言 VS 讯飞星火 VS chatgpt (28)-- 算法导论5.1 3题
    三、假设你希望以1/2的概率输出0与1。你可以自由使用一个输出0或1的过程BIASED-RANDOM。它以某概率p输出1,概率1-p输出0,其中0<p<1,但是p的值未知。请给出一个利用BIASED-RANDOM作为子程序的算法,返回一个无偏的结果,能以概率1/2返回0,以概率1/2返回1。作为p的函数,你的算......