最小生成树&最短路基础算法总结

最短路问题

解决最短路问题有以下算法:Dijkstra算法,Bellman-Ford算法,Floyd算法,和SPFA算法和启发式搜索算法A*;

每个算法都有它的特点可以解决某些特定的问题,例如:Floyd算法可以求解任意两点之间的最短路径长度,SPFA可以判定是否存在负环问题

一. Dijkstra 算法:

  解决的问题:<非负权图单源最短路>1.从某一点出发到所有点的最短路径,就是最后更新的dis数组2.从某一个点到出发到具体某一点的最短路,只要第一次吧这个点加入最短路就可以终止程序了。3.注意Dijkstar 算法不能处理负权边

  思想:贪心

  做法: 每次将未加入最短路径的点种找距离出发点最近的点加入,然后用这个点更新所有没有加入的点到起始点的最小距离,每次加入一个点更新一次,直到所有的点都加入数组后结束,就可以统计出这个点到所有点的最短路径了

  算法复杂度:斐波那契堆的复杂度O(E+VlgV)

  核心代码:

 1 void dijk(int s , int n)
 2 {
 3     int i , j , k ;
 4     for( i = 1 ; i <= n ;i++)
 5     {
 6         p[i] = false;
 7         dist[i] = mp[s][i];
 8     }
 9     p[s] = true;
10     dist[s] = 0;
11     for(i = 1 ; i < n ; i++)
12     {
13         int Min = INF;
14         int k = 0 ;
15         for( j = 1 ; j <= n ;j++)
16         {
17             if(!p[j]&&dist[j]<Min)
18             {
19                 Min = dist[j];
20                 k = j;
21             }
22         }
23         if(Min==INF) return ;
24         p[k] = true;
25         for(j = 1 ; j <= n ;j++)
26         {
27             if(!p[j]&&mp[k][j]!=INF&&dist[j]>dist[k]+mp[k][j])
28                 dist[j] = dist[k]+mp[k][j];
29         }
30     }
31 }
View Code 

二.BellmanFord 算法和SPFA 算法(Shortest Path Faster Algorithm)

  解决的问题:1.权值有负值得图的单源最短路,并且可以检测到负环,注意,由于B_F算法的复杂度过高而且都可以用SPFA解决,所以我们只学习SPFA算法(一个特例:bellman可以检测并输出负环,单SPFA不能输出负环)2.最长路

  思想:松弛操作

  做法:设置一个队列,开始将初始点加入这个队列中,如果队列不空的话,取出队列顶端的元素i,用队列顶端的元素对所有的点j进行松弛操作  if(mp[i][j]!=INF&&dis[j]>dis[i]+mp[i][j]) dis[j] = dis[i]+mp[i][j];  如果j点没有在队列中,说明它还有更新其他点的潜力,所以将被更新的j点也加入到队列中去,设置一个计数器,设总点数为n,如果一个点进队的次数大于n则说明存在负环。

  算法复杂度:O(kE) k是每个节点进队的次数,一般来说k<=2;但是这里的复杂度证明是有问题的,所以SPFA的最坏的情况应该是O(VE)

  核心代码:

1、初始化所有点,每一个点保存一个值,表示从源点到达这个点的距离,将源点的值设为0,其他点的值设为无穷大(表示不可达)
2、进行循环,循环n-1次。在循环内部,遍历所有的边,进行松弛计算
3、遍历图中所有的边,判断是否存在这样情况d[v]>d[u]+w(u,v); 
,则表示图中存在从源点可达的负权回路
Bellman-Ford 算法思路
 1 int SPFA()
 2 {
 3     memset(inq,0,sizeof(inq));
 4     for(int i =0 ;i <= n ;i++)
 5         dis[i] = INF;
 6     top = 0;
 7     dis[1] = 0;
 8     que[top++] = 1;
 9     inq[1]=true;
10     for(int i = 0;i != top ;i = i+1%N)//队列不为空,注意i和top不是同时循环到下一次的
11     {
12         int u = que[i];
13         inq[u]=false;
14         for(int j = head[u] ; j!=-1 ;j = edge[j].next)
15         {
16             Edge e = edge[j];
17             if(dis[e.to]>dis[u]+e.w)
18             {
19                 dis[e.to] = dis[u]+e.w;
20                 if(inq[e.to]==false)
21                 {
22                     que[top++] = e.to;
23                     top %= N;//循环队列
24                     inq[e.to] = true;
25                 }
26             }
27         }
28     }
29     return dis[n];
30 }
spfa

 三.Floyd 算法:

  解决的问题:1.全局最短路。2.最长路

  思想:动态规划

  做法:对于每个节点k,都对定点对[i,j]做一次松弛操作

  算法复杂度:O(n^3)

  核心算法:

 1 for (int k=0; k<n; ++k) {
 2   for (int i=0; i<n; ++i) {
 3     for (int j=0; j<n; ++j) {
 4             /*
 5             实际中为防止溢出,往往需要选判断 dist[i][k]和dist[k][j
 6             都不是Inf ,只要一个是Inf,那么就肯定不必更新。 
 7             */
 8       if (dist[i][k] + dist[k][j] < dist[i][j] ) {
 9         dist[i][j] = dist[i][k] + dist[k][j];
10       }
11     }
12   }
13 }
View Code

  说明:最短路的最优子结构(不只是对于Floyd,对于任何的最短路算法都有次性质)可以通过反证法证明,这里应用这个最优子结构的性质,来用Floyd算法来记录最短路的路径

 1 void floyd() {
 2       for(int i=1; i<=n ; i++){
 3         for(int j=1; j<= n; j++){
 4           if(map[i][j]==Inf){
 5                path[i][j] = -1;//表示  i -> j 不通 
 6           }else{
 7                path[i][j] = i;// 表示 i -> j 前驱为 i
 8           }
 9         }
10       }
11       for(int k=1; k<=n; k++) {
12         for(int i=1; i<=n; i++) {
13           for(int j=1; j<=n; j++) {
14             if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
15               dist[i][j] = dist[i][k] + dist[k][j];
16               path[i][k] = i;
17               path[i][j] = path[k][j];
18             }
19           }
20         }
21       }
22     }
23     void printPath(int from, int to) {
24         /*
25          * 这是倒序输出,若想正序可放入栈中,然后输出。
26          * 
27          * 这样的输出为什么正确呢?个人认为用到了最优子结构性质,
28          * 即最短路径的子路径仍然是最短路径
29          */
30         while(path[from][to]!=from) {
31             System.out.print(path[from][to] +"");
32             to = path[from][to];
33         }
34     }
View Code

当然,这里只是提供一个思路,其实对于所有的最短路保存路径都可以用类似是dfs保存路径的方法,通过保存前驱的方法,每次松弛操作的时候对前驱也进行修改就可以了

 【最小生成树问题】

解决最小生成树问题的算法有:prim算法和kruskal算法

一.prim 算法(普利姆算法)

  思想:贪心

  做法:类似于dijkstra算法,只不过是每次选取的点是距离生成树最近的点,更新的时候不再是更新这个点到起始点的最短距离,而是到这个生成输的最短距离。

  核心代码:

 1 void prim(int s , int n)
 2 {
 3     int i , j , k ;
 4     for( i = 1 ; i <= n ;i++)
 5     {
 6         p[i] = false;
 7         dist[i] = mp[s][i];
 8     }
 9     p[s] = true;
10     dist[s] = 0;
11     for(i = 1 ; i < n ; i++)
12     {
13         int Min = INF;
14         int k = 0 ;
15         for( j = 1 ; j <= n ;j++)
16         {
17             if(!p[j]&&dist[j]<Min)
18             {
19                 Min = dist[j];
20                 k = j;
21             }
22         }
23         if(Min==INF) return ;
24         p[k] = true;
25         for(j = 1 ; j <= n ;j++)
26         {
27             if(!p[j]&&mp[k][j]!=INF&&dist[j]>mp[k][j])
28                 dist[j] = mp[k][j];
29         }
30     }
31 }
View Code

  时间复杂度:邻接矩阵:O(v^2)     邻接表:O(elog2v)

二.kruskal 算法

  思想:并查集

  做法:将原图G中所有E条边按权值从小到大排序。循环:从权值最小的边开始遍历每条边,直至图G_new中所有的节点都在同一个连通分量中。每次从图中未加入树种的边种找到边权最小的看,这两个点的祖先是否是一个,如果是一个说明两个点已经是一个树上的了,不做操作,如果两个点来自不同的树即有不同的祖先,那么就把这两个点所代表的两个树合并起来,最后当所有的点都在一棵树上的时候停止操作,一般用合并次数来控制,即n个点需要合并n-1次,所以最好合并操作写在kruskal函数内部,这样方便统计步数

  核心代码:

 1 struct Edge{
 2     int from;
 3     int to;
 4     int w;
 5     bool operator < (const Edge &a) const
 6     {
 7         return w<a.w;
 8     }
 9 }edge[N*N];
10 int fa[N];
11 int Getfa(int x){return (fa[x]==x)?x:fa[x] = Getfa(fa[x]); }
12 int fl;
13 int n,m;
14 bool solve(int x){
15     int cnt = 0;//共合n-1次结束
16     for(int i = 1; i <= n; i++) fa[i] = i;//注意点是从1开始编号的
17     for(int i = x; i < m; i++){
18         int X = Getfa(edge[i].from);
19         int Y = Getfa(edge[i].to);
20         if(X != Y){
21             fa[X] = Y;
22             cnt++;
23             if(cnt==n-1){ fl = edge[i].w;return true;}
24         }
25     }
26     return false;
27 }
View Code

注意:对于图的题,一定要根据题意来选取是用链表存储还是用数组存储,选择好的存储方法便于解决问题,也要注意点的编号是从几开始的。

原文地址:https://www.cnblogs.com/shanyr/p/5212211.html