最短路问题+最小生成树

最短路问题

1.Dijkstra(迪杰斯特拉)算法(堆优化版)

基本思想:找到最短距离已经确定的顶点(最初只有起点),从它出发更新相邻顶点的最短距离。把每个顶点当前的最短距离用堆维护,在每次更新时往堆里插入当前最短距离和顶点的值对,而每次从堆中取出的最小值就是下一次要使用的顶点。当取出的最小值不是最短距离的话,就丢弃这个值。当堆为空时,算法结束。

  • 图的存储

链式前向星法
int N,M,dix; //图的最大顶点数和边数
int h[N]; //顶点数组
struct node{
int e,v,next;
}edge[M];
//添加边
void add(int a,int b,int c) //边起点、终点、权值
{
edge[++dix].e=b;
edge[dix].v=c;
edge[dix].next=h[a];
h[a]=dix;
}

例题洛谷P4479(此题用SPFA会卡死)
c++代码

#include 
using namespace std;
typedef pair<int,int> P;  //first是最短距离,second是顶点的编号

const int N=100010,M=200010,inf=INT_MAX;
int n,m,s,dix;
int h[N],e[M],v[M],next[M],d[N];  //用数组代替结构体,思想是一样的
priority_queue<P,vector<P>,greater<P> > q;  //堆按照first从小到大的顺序取出值

void add(int a,int b,int c){
	e[dix]=b,v[dix]=c,next[dix]=h[a],h[a]=dix++;
}

void Dijkstra(){
	while(!q.empty()){
		P p=q.top();
		q.pop();
		int t=p.second;
		if(d[t]<p.first) continue;  //当前取出的最小值不是最短距离,丢弃这个值
		for(int i=h[t];i!=-1;i=next[i]){
			if(d[e[i]]>d[t]+v[i]){
				d[e[i]]=d[t]+v[i];
				q.push({d[e[i]],e[i]});
			}
		}
	}
}

int main(){
	cin>>n>>m>>s;
	fill(d+1,d+n+1,inf);
	fill(h+1,h+n+1,-1);
	for(int i=0;i<m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	d[s]=0;
	q.push({d[s],s});
	Dijkstra();
	for(int i=1;i<=n;i++)
	cout<<d[i]<<" ";
	return 0;
}

2.SPFA(Shortest Path Faster Algorithm,最短路径快速算法)(Bellman_Ford算法的改进版)

基本思想:建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
判断有无负环:如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
例题洛谷P3371(上题的弱化版)
c++代码

#include 
using namespace std;

const int N=100010,M=200010,inf=INT_MAX;
int n,m,s,dix;
int h[N],e[M],v[M],next[M],d[N],vis_count[N];
bool vis[N];
queue<int> q;

void add(int a,int b,int c){
	e[dix]=b,v[dix]=c,next[dix]=h[a],h[a]=dix++;
}

bool spfa(){
	while(!q.empty()){
		int t=q.front();
		q.pop();
		if(vis_count[t]++>n) return true;  //返回正值代表存在从s可达的负圈
		vis[t]=false;
		for(int i=h[t];i!=-1;i=next[i]){
			if(d[e[i]]>d[t]+v[i]){
				d[e[i]]=d[t]+v[i];
				if(!vis[e[i]]){
					q.push(e[i]);
					v[e[i]]=true;
				}
			}
		}
	}
	return false;
}

int main(){
	cin>>n>>m>>s;
	fill(d+1,d+n+1,inf);
	fill(h+1,h+n+1,-1);
	for(int i=0;i<m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	d[s]=0;
	q.push(s);
    spfa();
	for(int i=1;i<=n;i++)
	cout<<d[i]<<" ";
	return 0;
}

3.两种算法的比较

Dijkstra算法的复杂度是O(|E|log|V|),SPFA的复杂度是O(Km)(k为常数,一般不大于2),最坏复杂度是O(nm),遇到异常数据会被卡死,像上面第一题,所以一般使用Dijkstra算法。但是,在图中存在负边的情况下,Dijkstra算法就无法正确求解问题,还是需要用SPFA。

4.任意两点间的最短路问题(Floyd_Warshall算法)

基本思想:

  • 从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。

  • 对于每一对顶点 i 和 j,看看是否存在一个顶点 k 使得从 i 到 k 再到 j 比己知的路径更短。如果是更新它。

注意点:

1. Floyd-Warshall算法时间复杂度:O(n^3)
2. Floyd-Warshall算法可以处理边是负数的情况。而判断图中是否有负圈,只需检查是否存在d[i][i]是负数的顶点i就可以了。

 //Floyd-Warshall算法核心语句
    for(k=1;k<=n;k++)
       for(i=1;i<=n;i++)
          for(j=1;j<=n;j++)
            if(e[i][k]<inf && e[k][j]<inf && e[i][j]>e[i][k]+e[k][j])
                e[i][j]=e[i][k]+e[k][j];

最小生成树

1.Kruskal算法(克鲁斯克尔算法)

基本思想:按照边的权值的顺序从小到大查看一遍,如果不产生圈(重边等也算在内),就把当前这条边加入到生成树中。
判断是否产生圈:假设现在要把连接顶点u和顶点v的边e加入生成树中。如果加入之前u和v不在同一个连通分量里,那么加入e也不会产生圈。反之,如果u和v在同一个连通分量里,那么一定会产生圈。可以使用并查集高效地判断是否属于同一个连通分量。
例题洛谷P3366
c++代码

#include 
using namespace std;
const int maxn=2e5;
int par[maxn],rank[maxn];
int n,m,res=0;
struct edge{
	int u,v,cost;
}es[maxn];

bool cmp(edge e1,edge e2){
	return e1.cost<e2.cost;
}

void init(int n){
	for(int i=0;i<n;i++){
		par[i]=i;
		rank[i]=0;
	}
}

int find(int x){
	if(par[x]==x) return x;
	return par[x]=find(par[x]);
}

void unite(int x,int y){
	x=find(x);
	y=find(y);
	if(x==y) return;
	if(rank[x]<rank[y]) par[x]=y;
	else{
		par[y]=x;
		if(rank[x]==rank[y]) rank[x]++;
	}
}

bool same(int x,int y){
	return find(x)==find(y);
}

int main(){
	cin>>n>>m;
	for(int i=0;i<m;i++)
	cin>>es[i].u>>es[i].v>>es[i].cost;
	sort(es,es+m,cmp);
	init(n);
	for(int i=0;i<m;i++){
		if(!same(es[i].u,es[i].v)){
			unite(es[i].u,es[i].v);
			res+=es[i].cost;
		}
	}
	cout<<res;
	return 0;
}

Kruskal算法的时间复杂度是O(|E|log|V|),适用于稀疏图。

2.Prim(普里姆)算法

等我想写了再补上(滑稽)
因为我感觉克鲁斯克尔算法已经够了qwq

2019.9.23 20:38 更新
上面那句话太打脸了,当遇到稠密图特别是完全图(图中每对顶点之间都有一条边)时,再用我最喜欢的Kruskal算法就要MLE了(难受)。
比如这道题 公路建路
没错就是这道题逼迫我去重新学了一遍Prim算法(完全图而且n=5000.。。。)
Prim算法基本思想:首先假设有一颗只包含一个顶点v的树T。然后贪心地选取T和其他顶点之间相连的最小权值的边,并把它加到T中。不断进行这个操作,就可以得到一颗生成树,可以证明通过这个方法得到的生成树就是最小生成树。
参考代码

#include 
using namespace std;
const int inf=0x7fffffff;
int n;
bool v[5001]; //v[i]表示点i是否在当前生成树集合中
double d[5001],x[5001],y[5001],ans; //d[i]表示点i到当前集合的最短边的长度,ans表示最小生成树中所有边的总长度

double dis(double x,double y,double x1,double y1){ //计算两点之间的长度
	return sqrt((x-x1)*(x-x1)+(y-y1)*(y-y1));
}

int main(){
	cin>>n;
	fill(d,d+n,inf);
	for(int i=0;i<n;i++){
		cin>>x[i]>>y[i];
	}
	d[0]=0;
	for(int i=0;i<n;i++){
		int t=-1,min_dis=inf;
		for(int j=0;j<n;j++)
		if(!v[j]&&d[j]<min_dis){
			t=j;
			min_dis=d[j];
		}
		v[t]=true;
		ans+=d[t];
		for(int j=0;j<n;j++){
			double temp=dis(x[t],y[t],x[j],y[j]);
		    if(!v[j]) d[j]=min(d[j],temp); 
		}
	}
	printf("%.2f",ans);
	return 0;
}

Prim算法时间复杂度:O(n^2),适用于稠密图。

如果想要得到更多知识,请关注我博客:wlis.blog.csdn.net

此博客不定期更新内容!!!感谢大家!!!

你可能感兴趣的:(算法,#,图论)