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