最短路与最小生成树

来源:互联网 发布:讲课软件 编辑:程序博客网 时间:2024/06/01 08:27

最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
最短路径是从一点出发,到达目的地的路径最小。
一句话概括:最小生成树是计算从一节点到另一节点的最小边集;最短路是带权路径,计算权值最小。
也就是说,最小生成树要经过每一个点,而最短路只需要能达到某两点,路径权值最小即可!**

最短路的算法有Floyd、Dijkstra、Bellman,SPFA
单源就是从一个点到所有其他点的最短路径,得到的结果是一个数组,表示某个点到其他点的最短距离。常用的算法有Dijkstra算法和Bellmanford算法。
多源最短路径计算所有点到其他点的最短距离,得到的是一个矩阵。常用的算法有Floyd算法


Dijkstra:适用于权值为非负的图的单源最短路径,用斐波那契堆的复杂度O(E+VlgV)
BellmanFord:适用于权值有负值的图的单源最短路径,并且能够检测负圈,复杂度O(VE)
SPFA:适用于权值有负值,且没有负圈的图的单源最短路径,论文中的复杂度O(kE),k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).
Floyd:每对节点之间的最短路径。

先给出结论:
(1)当权值为非负时,用Dijkstra。
(2)当权值有负值,且没有负圈,则用SPFA,SPFA能检测负圈,但是不能输出负圈。
(3)当权值有负值,而且可能存在负圈,则用BellmanFord,能够检测并输出负圈。
(4)SPFA检测负环:当存在一个点入队大于等于V次,则有负环

Floyd——多源最短路径
只有五行的算法——Floyd-Warshall

Floyd算法用来找出每对顶点之间的最短距离,它对图的要求是,既可以是无向图也可以是有向图,边权可以为负,但是不能存在负环(可根据最小环的正负来判定).

以所有定点为中转(k),求得任意两点之间的最短路径(i,j)
核心代码:

for(int k=1; k<=n; k++)    for(int i=1; i<=n; i++)        for(int i=1; j<=n; j++)            if(e[i][j]>e[i][k]+e[k][j])

Dijkstra
每次找 距离源点最近的一个顶点,然后以该顶点为中心进行扩展,(在集合Q中的所有顶点中选择里源点s最近的顶点u,加入到集合P,
并考虑所有以u为起点的边,对每一条边进行松弛操作。重复此过程直到Q为空)最终得到源点到其余所有点的最短路径。

#include<cstdio>#include<cmath>#include<cstring>#include<iostream>#include<algorithm>using namespace std;#define inf 0x3f3f3f;int g[1005][1005];int vis[1005];int dis[1005];int x,y,len,n,t;int temp,minj;int main(){  while(~scanf("%d%d",&t,&n))  {   memset(vis,0,sizeof(vis));   memset(dis,0,sizeof(dis));   memset(g,0,sizeof(g));   //初始化   for(int i=1;i<=n;i++)   {      for(int j=1;j<=n;j++)      {          if(i==j)          g[i][j]=0;          else            g[i][j]=inf;      }   }    //读入边   for(int i=1;i<=t;i++)   {      scanf("%d%d%d",&x,&y,&len);      if(g[x][y]>len)        g[x][y]=g[y][x]=len;   }   for(int i=1;i<=n;i++)    dis[i]=g[1][i];   vis[1]=1;   for(int i=1;i<=n;i++)   {      minj=inf;      for(int j=1;j<=n;j++)      {          if(vis[j]==0&&dis[j]<minj)          {              minj=dis[j];              temp=j;          }      }      vis[temp]=1;     for(int j=1;j<=n;j++)     {        if(vis[j]==0)            dis[j]=min(dis[j],dis[temp]+g[temp][j]);     }   }   printf("%d\n",dis[n]);  }}

Bellman——解决负权边
Dijkstra虽然好,但是不能解决带负权边的图,每次松弛以上次的确定的某些最短路计算出下面的最短路。
核心代码:

for(int k=1; k<=n-1; k++)
for(int i=1; i<=m; i++)
if(dis[v[i]]>dis[u[i]]+w[i])
dis[v[i]]=dis[u[i]]+w[i];
外环循环了n(点的个数)次,内环循环了m(边的个数)次,
松弛操作只需进行n-1次就行了。因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边。
另外,Bellman算法可以检验一个图中是否含有负权边。
如果进行n-1轮松弛后,仍存在if(dis[v[i]]>dis[u[i]]+w[i]) dis[v[i]]=dis[u[i]]+w[i];的情况,
说明在进行n-1轮松弛后,仍然可以继续松弛成功,那么此图必然存在负权回路,

关键代码:

```//Bellman核心代码for(int k=1; k<=n-1; k++)    for(int i=1; i<=m; i++)        if(dis[v[i]]>dis[u[i]]+w[i])        dis[v[i]]=dis[u[i]]+w[i];//检测负权回路 flag=0;for(i=1;i<=m;i++)    if(dis[v[i]]>dis[u[i]]+w[i])    flag=1;if(flag==1)    printf("此图有负权回路");

SPFA 用两副代码帮助组理解运用,其中有用到 邻接表
邻接表理解可见

1.此代码理解起来比较顺利

include<stdio.h>using namespace std;int n,m,i,j,k;int u[8],v[8],w[8];int first[6];//比 n 大 1;int next[8]; //比m大1;int dis[6]= {0};int book[6]= {0}; //标记是否在队列中int que[101]= {0},head=1,tail=1;int inf=9999999;void bfs(){    //1号顶点入队    que[tail]=1;    tail++;    book[1]=1;    while(head<tail)//队列不为空时    {        k=first[que[head]];//当前需要处理的队首顶点        while(k!=-1)//扫描当前顶点所有的边        {            if(dis[v[k]]>dis[u[k]]+w[k])            {                dis[v[k]]=dis[u[k]]+w[k];//更新顶点1到顶点v[k]的路程                //book盘点顶点v[k]是否在队列中                if(book[v[k]]==0)//表示V[k]不在队中,                {                    //入队                    que[tail]=v[k];                    book[v[k]]=1;                    tail++;                }            }            k=next[k] ;        }        //出队;        book[que[head]]=0;        head++;    }}int main(){    scanf("%d%d",&n,&m);    for(int i=1; i<=n; i++)        dis[i]=inf;    dis[1]=0;    for(int i=1; i<=n; i++)        book[i]=0;    for(int i=1; i<=n; i++)        first[i]=-1;//-1表示1~n暂时没边    for(int i=1; i<=m; i++)    {        scanf("%d%d%d",&u[i],&v[i],&w[i]);        next[i]=first[u[i]];//建立邻接表关键        first[u[i]]=i;//    }    bfs();    // for(int i=1; i<=n; i++)    printf("%d ",dis[n]);    printf("\n");    getchar();    getchar();}

2.此代码本人默认

#include<cstdio>#include<cstring>#include <iostream>#include<cmath>#include<algorithm>#include <queue>using namespace std;const long MAXN=10005;const long lmax=0x7FFFFFFF;typedef struct{    long v;    long next;    long cost;} Edge;Edge e[MAXN];long p[MAXN];long Dis[MAXN];bool vist[MAXN];queue<long> q;long m,n;//点,边void init(){    long i;    long eid=0;    memset(vist,0,sizeof(vist));    memset(p,-1,sizeof(p));    fill(Dis,Dis+MAXN,lmax);    while (!q.empty())    {        q.pop();    }    for (i=0; i<n; ++i)    {        long from,to,cost;        scanf("%ld %ld %ld",&from,&to,&cost);        e[eid].next=p[from];        e[eid].v=to;        e[eid].cost=cost;        p[from]=eid++;//以下适用于无向图        swap(from,to);        e[eid].next=p[from];        e[eid].v=to;        e[eid].cost=cost;        p[from]=eid++;    }}void print(long End){//若为lmax 则不可达    printf("%ld\n",Dis[End]);}void SPF(){    init();    long Start,End;    scanf("%ld %ld",&Start,&End);    Dis[Start]=0;    vist[Start]=true;    q.push(Start);    while (!q.empty())    {        long t=q.front();        q.pop();        vist[t]=false;        long j;        for (j=p[t]; j!=-1; j=e[j].next)        {            long w=e[j].cost;            if (w+Dis[t]<Dis[e[j].v])            {                Dis[e[j].v]=w+Dis[t];                if (!vist[e[j].v])                {                    vist[e[j].v]=true;                    q.push(e[j].v);                }            }        }    }    print(End);}int main(){    while (scanf("%ld %ld",&m,&n)!=EOF)    {        SPF();    }    return 0;}

最小生成树 算法包括kruskal、prim

Kruskal算法按照边的权值的顺序从小到大查看一遍,如果不产生重边,就把当前这条边加入到生成树中。
typedef struct edge

{      int a;      int b;      int value;  }edge;  edge edges[earraysize];  int final[narraysize];            //存储父节点 中括号里面是儿子,外面是父亲  int nodecount[narraysize];        //存储该节点孩子结点的个数   bool cmp(edge a,edge b)  {       return a.value<b.value;  }  int findp(int x)    //寻找父亲  {      while(x!=fa[x])          x=fa[x];      return x;  }  bool Union(int x,int y)          //合并   {      int rootx=findp(x);          /*为什么要找父亲?因为要判是否有回路,假如父亲相同,而x跟y连通,那么就形成了回路*/      int rooty=findp(y);      if(rootx==rooty)          return false;      else if(nodecount[rootx]<=nodecount[rooty])      //优化,把深度小的子树加到深度大的子树,减少树的高度      {          final[rootx]=rooty;                         /*其实不优化也可以直接final[rootx]=rooty或者final[rooty]=rootx也ok */          nodecount[rooty]+=nodecount[rootx];      }      else      {          final[rooty]=rootx;          nodecount[rootx]+=nodecount[rooty];      }      return true;  }  int main ()  {      //freopen("a.txt","r",stdin);      int num=0;      int n,m;      int i,j;      while ( scanf ( "%d%d", &n, &m ) != EOF )      {          num=0;              //记录生成树中的边的数目          for(i=1;i<=m;i++)          {              scanf("%d%d%d",&edges[i].a,&edges[i].b,&edges[i].value);          }          for(i=1;i<=n;i++)      //初始化                          {              final[i]=i;              nodecount[i]=1;          }          sort(edges+1,edges+m+1,cmp);   //排序                                       for(i=1;i<=m;i++)              //遍历所有的边           {              if(Union(edges[i].a,edges[i].b))         //合并               {                  num++;              }              if(num==n-1)               //找到了最小生成树                   break;          }      }      return 0;  }

补充一个便于理解的

#include<cstdio>struct node{    int u,v,w;}e[101];//范围比m大一int  n,m;int f[7]={0};//范围比n大一int sum=0;int count=0;//快排void quicksort(int left,int right){    int i,j;    struct node t;    if(left>right)        return;    i=left;    j=right;    while(i!=j)    { //顺序很重要,要先从右边开始找        while(e[j].w>e[left].w&&i<j)        {            j--;        }        while(e[i].w<e[left].w&&i<j)        {            i++;        }        //交换        if(i<j)        {            t=e[i];            e[i]=e[j];            e[j]=t;        }    }    t=e[left];    e[left]=e[i];    e[i]=t;    quicksort(left,i-1);//继续处理左边    quicksort(i+1,right);//继续处理右边    return;}int find(int v){    if(v==f[v])        return v;    else    {//路径压缩        f[v]=find(f[v]);        return f[v];    }}int merge(int v,int u){    int t1,t2;    t1=find(v);    t2=find(u);    if(t1!=t2)//判断两个点是否在一个集合中    {        f[t2]=t1;      return 1;    }    return 0;}int main(){    int  i;    scanf("%d%d",&n,&m);    for(int i=1;i<=m;i++)    scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);    quicksort(1,m);按权值从小到大排序    for(int i=1;i<=n;i++)//初始化        f[i]=i;//Kruskal算法核心    for(int i=1;i<=m;i++)    {//判断一条边的两个顶点是否已经连通,即是否在一个集合中        if(merge(e[i].u,e[i].v))        {            count++;            sum+=e[i].w;        }        if(count==n-1)//直到选用了n-1条边智之后退出循环            break;    }    printf("%d\n",sum);}

Prim算法
用dis记录“生成树”到各个顶点的距离,与Dijkstra不同,不是每个顶点到1号的最短距离,
而是每个顶点到任意一个“树顶点”(已被选入生成树的顶点)的最短距离,
如果dis[k]>e[i][j]则更新dis[k]=e[i][j];在计算式更新最短路径的时候不用加上dis[j],
因为我们的目的非要靠近1号顶点,而是靠近“生成树”就可以了,
也就是说只要靠近生成树中的任意一个“树顶点”就行了。
重点步骤:从数组中选出离生成树最近的顶点j,加入到生成树中,再以j为中间点,
更新生成树到没一个非树顶点的距离(松弛), (即 如果dis[k]>e[i][j]则更新dis[k]=e[i][j];)
重复此步骤,直到生成树中有n个顶点。

#define INF 0x1f1f1f1f#define M 1000using namespace std;double dis[M],map[M][M];bool flag[M];int prim(int s,int n)                        //s为起点,n为点的个数{    int i,j,k,temp,md,total=0;    for(i=1; i<=n; i++)        dis[i]=map[s][i];                    //与最短路不同,而是将dis置为map[s][i]    memset(flag,false,sizeof(flag));    flag[s]=true;                            //将起点加入集合    for(i=1; i<n; i++)                       //依旧进行n-1次迭代,每次找到不在集合的最小边(n个点有n-1条边)!!!!!!    {        md=INF;        for(j=1; j<=n; j++)        {            if(!flag[j]&&dis[j]<md)            {                md=dis[j];                temp=j;            }        }        flag[temp]=true;                      //将找到的最小边的点加入集合        total+=md;                            //并将这个边的权值加到total中        for(j=1; j<=n; j++)                   //松弛操作,注意与最短路不同            if(!flag[j]&&dis[j]>map[temp][j])                dis[j]=map[temp][j];    }    return total;}
原创粉丝点击