1. 程式人生 > >最短路與最小生成樹

最短路與最小生成樹

最小生成樹能夠保證整個拓撲圖的所有路徑之和最小,但不能保證任意兩點之間是最短路徑。
最短路徑是從一點出發,到達目的地的路徑最小。
一句話概括:最小生成樹是計算從一節點到另一節點的最小邊集;最短路是帶權路徑,計算權值最小。
也就是說,最小生成樹要經過每一個點,而最短路只需要能達到某兩點,路徑權值最小即可!**

最短路的演算法有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)//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 1000
using 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;
}