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


    最短路问题

    解决最短路问题有以下算法: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

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

  • 相关阅读:
    12迭代器
    11(2)Vector(向量)
    11(1) LinkList ---链表
    11集合(Collection<E>) Arraylist
    10异常
    乘法计算过程的模拟
    10 Date详解
    详细的OA系统学习
    8 math类
    Java开发中的23种设计模式详解
  • 原文地址:https://www.cnblogs.com/shanyr/p/5212211.html
Copyright © 2020-2023  润新知