




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
六、圖論王振昊第一節基本概念一、什么是圖?很簡單,點用邊連起來就叫做圖,嚴格意義上講,圖是一種數據結構,定義為:graph=(V,E)。V是一個非空有限集合,代表頂點(結點),E代表邊的集合。二、圖的一些定義和概念(a)有向圖:圖的邊有方向,只能按箭頭方向從一點到另一點。(a)就是一個有向圖。(b)無向圖:圖的邊沒有方向,可以雙向。(b)就是一個無向圖。結點的度:無向圖中與結點相連的邊的數目,稱為結點的度。結點的入度:在有向圖中,以這個結點為終點的有向邊的數目。結點的出度:在有向圖中,以這個結點為起點的有向邊的數目。權值:邊的“費用”,可以形象地理解為邊的長度。連通:如果圖中結點U,V之間存在一條從U通過若干條邊、點到達V的通路,則稱U、V是連通的。回路:起點和終點相同的路徑,稱為回路,或“環”。完全圖:一個n階的完全無向圖含有n*(n-1)/2條邊;一個n階的完全有向圖含有n*(n-1)條邊;稠密圖:一個邊數接近完全圖的圖。稀疏圖:一個邊數遠遠少于完全圖的圖。
強連通分量:有向圖中任意兩點都連通的最大子圖。右圖中,1-2-5構成一個強連通分量。特殊地,單個點也算一個強連通分量,所以右圖有三個強連通分量:1-2-5,4,3。12345(a)12345(b)12345
三、圖的存儲結構1.二維數組鄰接矩陣存儲定義intG[101][101];G[i][j]的值,表示從點i到點j的邊的權值,定義如下:
上圖中的3個圖對應的鄰接矩陣分別如下:0111011∞58∞3G(A)=1011G(B)=0015∞2∞61100010G(C)=82∞1041100∞∞10∞1136411∞下面是建立圖的鄰接矩陣的參考程序段:#include<iostream>usingnamespacestd;inti,j,k,e,n;doubleg[101][101];doublew;intmain(){inti,j;for(i=1;i<=n;i++)for(j=1;j<=n;j++)g[i][j]=0x7fffffff(賦一個超大值);//初始化,對于不帶權的圖g[i][j]=0,表示沒有邊連通。這里用0x7fffffff代替無窮大。cin>>e;for(k=1;k<=e;k++){cin>>i>>j>>w;//讀入兩個頂點序號及權值g[i][j]=w;//對于不帶權的圖g[i][j]=1g[j][i]=w;//無向圖的對稱性,如果是有向圖則不要有這句!}…………return0;}建立鄰接矩陣時,有兩個小技巧:初始化數組大可不必使用兩重for循環。1)如果是int數組,采用memset(g,0x7f,sizeof(g))可全部初始化為一個很大的數(略小于0x7fffffff),使用memset(g,0,sizeof(g)),全部清為0,使用memset(g,0xaf,sizeof(g)),全部初始化為一個很小的數。2)如果是double數組,采用memset(g,127,sizeof(g));可全部初始化為一個很大的數1.38*10306,使用memset(g,0,sizeof(g))全部清為0.2.數組模擬鄰接表存儲圖的鄰接表存儲法,又叫鏈式存儲法。本來是要采用鏈表實現的,但大多數情況下只要用數組模擬即可。定義兩個數組,例如:intg[101][101];用來存儲這個圖。再定義一個一維數組:intnum[101];用來記錄與每個點相連的點的數目。如果邊有權值,還要定義一個數組intval[101][101]存儲邊權。以下是用數組模擬鄰接表存儲的參考程序段:#include<iostream>usingnamespacestd;intn,m,i,j,c;intg[101][101];intnum[101];intmain(){memset(g,0x7f,sizeof(g));memset(num,0,sizeof(num));cin>>n;for(i=1;i<=n;i++){cin>>num[i];//第i行說明點i的連接情況,每行的開頭讀入與之相連的點的數目for(j=1;j<=num[i];j++){cin>>g[i][j];//第j個與i相連的點是g[i][j]val[i][g[i][j]]=v;//有權圖還要存下權值}}…………return0;}兩種方法各有用武之地,需按具體情況,具體選用。第二節圖的遍歷一、深度優先與廣度優先遍歷從圖中某一頂點出發系統地訪問圖中所有頂點,使每個頂點恰好被訪問一次,這種運算操作被稱為圖的遍歷。為了避免重復訪問某個頂點,可以設一個標志數組visited[i],未訪問時值為false,訪問一次后就改為true。圖的遍歷分為深度優先遍歷和廣度優先遍歷兩種方法,兩者的時間效率都是O(n*n)。1.深度優先遍歷深度優先遍歷與深搜DFS相似,從一個點A出發,將這個點標為已訪問visited[i]:=true;,然后再訪問所有與之相連,且未被訪問過的點。當A的所有鄰接點都被訪問過后,再退回到A的上一個點(假設是B),再從B的另一個未被訪問的鄰接點出發,繼續遍歷。例如對右邊的這個無向圖深度優先遍歷,假定先從1出發程序以如下順序遍歷:
1→2→5,然后退回到2,退回到1。從1開始再訪問未被訪問過的點3,3沒有未訪問的鄰接點,退回1。再從1開始訪問未被訪問過的點4,再退回1。起點1的所有鄰接點都已訪問,遍歷結束。12345下面給出的深度優先遍歷的參考程序,假設圖以鄰接表存儲voiddfs(inti)//圖用數組模擬鄰接表存儲,訪問點i{visited[i]=true;//標記為已經訪問過for(intj=1;j<=num[i];j++)//遍歷與i相關聯的所有未訪問過的頂點if(!visited[g[i][j]])dfs(g[i][j]);}主程序如下:intmain(){……memset(visited,false,sizeof(visited));for(inti=1;i<=n;i++)//每一個點都作為起點嘗試訪問,因為不是從任何//一點開始都能遍歷整個圖的,例如下面的兩個圖。if(!visited[i])dfs(i);……return0;}12345以3為起點根本不能遍歷整個圖這個非連通無向圖任何一個點為起點都不能遍歷整個圖145232.廣度優先遍歷廣度優先遍歷并不常用,從編程復雜度的角度考慮,通常采用的是深度優先遍歷。廣度優先遍歷和廣搜BFS相似,因此使用廣度優先遍歷一張圖并不需要掌握什么新的知識,在原有的廣度優先搜索的基礎上,做一點小小的修改,就成了廣度優先遍歷算法。二、一筆畫問題
如果一個圖存在一筆畫,則一筆畫的路徑叫做歐拉路,如果最后又回到起點,那這個路徑叫做歐拉回路。我們定義奇點是指跟這個點相連的邊數目有奇數個的點。對于能夠一筆畫的圖,我們有以下兩個定理。定理1:存在歐拉路的條件:圖是連通的,有且只有2個奇點。定理2:存在歐拉回路的條件:圖是連通的,有0個奇點。兩個定理的正確性是顯而易見的,既然每條邊都要經過一次,那么對于歐拉路,除了起點和終點外,每個點如果進入了一次,顯然一定要出去一次,顯然是偶點。對于歐拉回路,每個點進入和出去次數一定都是相等的,顯然沒有奇點。求歐拉路的算法很簡單,使用深度優先遍歷即可。根據一筆畫的兩個定理,如果尋找歐拉回路,對任意一個點執行深度優先遍歷;找歐拉路,則對一個奇點執行DFS,時間復雜度為O(m+n),m為邊數,n是點數。以下是尋找一個圖的歐拉路的算法實現:樣例輸入:第一行n,m,有n個點,m條邊,以下m行描述每條邊連接的兩點。
551223344551樣例輸出:歐拉路或歐拉回路
154321【參考程序】Euler.cpp#include<iostream>#include<cstring>usingnamespacestd;#definemaxn101intg[maxn][maxn];//此圖用鄰接矩陣存儲intdu[maxn];//記錄每個點的度,就是相連的邊的數目intcircuit[maxn];//用來記錄找到的歐拉路的路徑intn,e,circuitpos,i,j,x,y,start;voidfind_circuit(inti)//這個點深度優先遍歷過程尋找歐拉路{intj;for(j=1;j<=n;j++)if(g[i][j]==1)//從任意一個與它相連的點出發{g[j][i]=g[i][j]=0;find_circuit(j);}circuit[++circuitpos]=i;//記錄下路徑}intmain(){memset(g,0,sizeof(g));cin>>n>>e;for(i=1;i<=e;i++){cin>>x>>y;g[y][x]=g[x][y]=1;du[x]++;//統計每個點的度du[y]++;}start=1;//如果有奇點,就從奇點開始尋找,這樣找到的就是for(i=1;i<=n;i++)//歐拉路。沒有奇點就從任意點開始,if(du[i]%2==1)//這樣找到的就是歐拉回路。(因為每一個點都是偶點)start=i;circuitpos=0;find_circuit(start);for(i=1;i<=circuitpos;i++)cout<<circuit[i]<<'';cout<<endl;return0;}注意以上程序具有一定的局限性,對于下面這種情況它不能很好地處理:上圖具有多個歐拉回路,而本程序只能找到一個回路。讀者在遇到具體問題時,還應對程序作出相應的修改。三、哈密爾頓環歐拉回路是指不重復地走過所有路徑的回路,而哈密爾頓環是指不重復地走過所有的點,并且最后還能回到起點的回路。使用簡單的深度優先搜索,就能求出一張圖中所有的哈密爾頓環。下面給出一段參考程序:#include<iostream>#include<cstring>usingnamespacestd;intstart,length,x,n;boolvisited[101],v1[101];intans[101],num[101];intg[101][101];voidprint(){inti;for(i=1;i<=length;i++)cout<<''<<ans[i];cout<<endl;}voiddfs(intlast,inti)//圖用數組模擬鄰接表存儲,訪問點i,last表示上次訪問的點{visited[i]=true;//標記為已經訪問過v1[i]=true;//標記為已在一張圖中出現過ans[++length]=i;//記錄下答案for(intj=1;j<=num[i];j++){if(g[i][j]==x&&g[i][j]!=last)//回到起點,構成哈密爾頓環{ ans[++length]=g[i][j];print();//這里說明找到了一個環,則輸出ans數組。 length--; break; }if(!visited[g[i][j]])dfs(i,g[i][j]);//遍歷與i相關聯的所有未訪問過的頂點}length--;visited[i]=false;//這里是回溯過程,注意v1的值不恢復。}intmain(){memset(visited,false,sizeof(visited));memset(v1,false,sizeof(v1));for(x=1;x<=n;x++)//每一個點都作為起點嘗試訪問,因為不是從任何一點開始都能找過整個圖的if(!v1[x])//如果點x不在之前曾經被訪問過的圖里。{length=0;//定義一個ans數組存答案,length記答案的長度。dfs(x);}return0;}第三節最短路徑算法如下圖所示,我們把邊帶有權值的圖稱為帶權圖。邊的權值可以理解為兩點之間的距離。一張圖中任意兩點間會有不同的路徑相連。最短路徑就是指連接兩點的這些路徑中最短的一條。
我們有四種算法可以有效地解決最短路徑問題。有一點需要讀者特別注意:邊的權值可以為負。當出現負邊權時,有些算法不適用。一、求出最短路徑的長度以下沒有特別說明的話,dis[u][v]表示從u到v最短路徑長度,w[u][v]表示連接u,v的邊的長度。1.Floyed-Warshall算法O(N3)簡稱Floyed(弗洛伊德)算法,是最簡單的最短路徑算法,可以計算圖中任意兩點間的最短路徑。Floyed的時間復雜度是O(N3),適用于出現負邊權的情況。算法描述:初始化:點u、v如果有邊相連,則dis[u][v]=w[u][v]。如果不相連則dis[u][v]=0x7fffffffFor(k=1;k<=n;k++)For(i=1;i<=n;i++) For(j=1;j<=n;j++) If(dis[i][j]>dis[i][k]+dis[k][j]) dis[i][j]=dis[i][k]+dis[k][j];算法結束:dis[i][j]得出的就是從i到j的最短路徑。算法分析&思想講解:三層循環,第一層循環中間點k,第二第三層循環起點終點i、j,算法的思想很容易理解:如果點i到點k的距離加上點k到點j的距離小于原先點i到點j的距離,那么就用這個更短的路徑長度來更新原先點i到點j的距離。在上圖中,因為dis[1][3]+dis[3][2]<dis[1][2],所以就用dis[1][3]+dis[3][2]來更新原先1到2的距離。我們在初始化時,把不相連的點之間的距離設為一個很大的數,不妨可以看作這兩點相隔很遠很遠,如果兩者之間有最短路徑的話,就會更新成最短路徑的長度。Floyed算法的時間復雜度是O(N3)。123216Floyed算法變形:如果是一個沒有邊權的圖,把相連的兩點間的距離設為dis[i][j]=true,不相連的兩點設為dis[i][j]=false,用Floyed算法的變形:For(k=1;k<=n;k++)For(i=1;i<=n;i++)For(j=1;j<=n;j++)dis[i][j]=dis[i][j]||(dis[i][k]&&dis[k][j]);用這個辦法可以判斷一張圖中的兩點是否相連。最后再強調一點:用來循環中間點的變量k必須放在最外面一層循環。【例4-1】、最短路徑問題【問題描述】平面上有n個點(n<=100),每個點的坐標均在-10000~10000之間。其中的一些點之間有連線。若有連線,則表示可從一個點到達另一個點,即兩點間有通路,通路的距離為兩點間的直線距離。現在的任務是找出從一點到另一點之間的最短路徑。【輸入格式】輸入文件為short.in,共n+m+3行,其中:第一行為整數n。第2行到第n+1行(共n行),每行兩個整數x和y,描述了一個點的坐標。第n+2行為一個整數m,表示圖中連線的個數。此后的m行,每行描述一條連線,由兩個整數i和j組成,表示第i個點和第j個點之間有連線。最后一行:兩個整數s和t,分別表示源點和目標點。【輸出格式】輸出文件為short.out,僅一行,一個實數(保留兩位小數),表示從s到t的最短路徑長度。【輸入樣例】500202202315121314253515【輸出樣例】3.41【參考程序】#include<cstdio>#include<iostream>#include<cmath>#include<cstring>usingnamespacestd;inta[101][3];doublef[101][101];intn,i,j,k,x,y,m,s,e;intmain(){freopen("short.in","r",stdin);freopen("short.out","w",stdout);cin>>n;for(i=1;i<=n;i++)cin>>a[i][1]>>a[i][2];cin>>m;memset(f,0x7f,sizeof(f));//初始化f數組為最大值
for(i=1;i<=m;i++)//預處理出x、y間距離{cin>>x>>y;f[y][x]=f[x][y]=sqrt(pow(double(a[x][1]-a[y][1]),2)+pow(double(a[x][2]-a[y][2]),2));//pow(x,y)表示x^y,其中x,y必須為double類型,要用cmath庫}cin>>s>>e;for(k=1;k<=n;k++)//floyed最短路算法for(i=1;i<=n;i++)for(j=1;j<=n;j++)if((i!=j)&&(i!=k)&&(j!=k)&&(f[i][k]+f[k][j]<f[i][j]))f[i][j]=f[i][k]+f[k][j];printf("%.2lf\n",f[s][e]);return0;}【例4-2】牛的旅行【問題描述】農民John的農場里有很多牧區。有的路徑連接一些特定的牧區。一片所有連通的牧區稱為一個牧場。但是就目前而言,你能看到至少有兩個牧區不連通。現在,John想在農場里添加一條路徑(注意,恰好一條)。對這條路徑有這樣的限制:一個牧場的直徑就是牧場中最遠的兩個牧區的距離(本題中所提到的所有距離指的都是最短的距離)。考慮如下的兩個牧場,圖1是有5個牧區的牧場,牧區用“*”表示,路徑用直線表示。每一個牧區都有自己的坐標:圖1所示的牧場的直徑大約是12.07106,最遠的兩個牧區是A和E,它們之間的最短路徑是A-B-E。這兩個牧場都在John的農場上。John將會在兩個牧場中各選一個牧區,然后用一條路徑連起來,使得連通后這個新的更大的牧場有最小的直徑。注意,如果兩條路徑中途相交,我們不認為它們是連通的。只有兩條路徑在同一個牧區相交,我們才認為它們是連通的。現在請你編程找出一條連接兩個不同牧場的路徑,使得連上這條路徑后,這個更大的新牧場有最小的直徑。【輸入格式】第1行:一個整數N(1<=N<=150),表示牧區數;第2到N+1行:每行兩個整數X,Y(0<=X,Y<=100000),表示N個牧區的坐標。每個牧區的坐標都是不一樣的。第N+2行到第2*N+1行:每行包括N個數字(0或1)表示一個對稱鄰接矩陣。例如,題目描述中的兩個牧場的矩陣描述如下:ABCDEFGH
A01000000
B10111000
C01001000
D01001000
E01110000
F00000010
G00000101
H00000010
輸入數據中至少包括兩個不連通的牧區。【輸出格式】只有一行,包括一個實數,表示所求答案。數字保留六位小數。【輸入樣例】
8
1010
1510
2010
1515
2015
3015
2510
3010
01000000
10111000
01001000
01001000
01110000
00000010
00000101
00000010【輸出樣例】
22.071068【算法分析】用Floyd求出任兩點間的最短路,然后求出每個點到所有可達的點的最大距離,記做mdis[i]。(Floyd算法)r1=max(mdis[i])然后枚舉不連通的兩點i,j,把他們連通,則新的直徑是mdis[i]+mdis[j]+(i,j)間的距離。r2=min(mdis[i]+mdis[j]+dis[i,j])re=max(r1,r2)re就是所求。【參考程序】#include<iostream>#include<cmath>usingnamespacestd;doublef[151][151],m[151],minx,r,temp,x[151],y[151],maxint=1e12;doubledist(inti,intj){returnsqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));}intmain(){inti,j,n,k;charc;cin>>n;for(i=1;i<=n;i++)cin>>x[i]>>y[i];for(i=1;i<=n;i++)for(j=1;j<=n;j++){cin>>c;if(c=='1')f[i][j]=dist(i,j);elsef[i][j]=maxint;}for(k=1;k<=n;k++)for(i=1;i<=n;i++)for(j=1;j<=n;j++)if(i!=j&&i!=k&&j!=k)if(f[i][k]<maxint-1&&f[k][j]<maxint-1)if(f[i][j]>f[i][k]+f[k][j])f[i][j]=f[i][k]+f[k][j];
memset(m,0,sizeof(m));for(i=1;i<=n;i++)for(j=1;j<=n;j++)if(f[i][j]<maxint-1&&m[i]<f[i][j])m[i]=f[i][j];minx=1e20;for(i=1;i<=n;i++)for(j=1;j<=n;j++)if(i!=j&&f[i][j]>maxint-1){temp=dist(i,j);if(minx>m[i]+m[j]+temp)minx=m[i]+m[j]+temp;}r=0;for(i=1;i<=n;i++)if(m[i]>minx)minx=m[i];printf("%.6lf",minx);return0;}2.Dijkstra算法O(N2)用來計算從一個點到其他所有點的最短路徑的算法,是一種單源最短路徑算法。也就是說,只能計算起點只有一個的情況。Dijkstra的時間復雜度是O(N2),它不能處理存在負邊權的情況。算法描述:
設起點為s,dis[v]表示從s到v的最短路徑,pre[v]為v的前驅節點,用來輸出路徑。
a)初始化:dis[v]=∞(v≠s);dis[s]=0;pre[s]=0;
b)For(i=1;i<=n;i++)1.在沒有被訪問過的點中找一個頂點u使得dis[u]是最小的。
2.u標記為已確定最短路徑
3.For與u相連的每個未確定最短路徑的頂點vif(dis[u]+w[u][v]
<
dis[v])
{dis[v]
=
dis[u]
+
w[u][v];pre[v]
=
u;
}
c)算法結束:dis[v]為s到v的最短距離;pre[v]為v的前驅節點,用來輸出路徑。算法分析&思想講解:從起點到一個點的最短路徑一定會經過至少一個“中轉點”(例如下圖1到5的最短路徑,中轉點是2。特殊地,我們認為起點1也是一個“中轉點”)。顯而易見,如果我們想求出起點到一個點的最短路徑,那我們必然要先求出中轉點的最短路徑(例如我們必須先求出點2的最短路徑后,才能求出從起點到5的最短路徑)。中轉點終點最短路1101221、2331、2、3441、254
換句話說,如果起點1到某一點V0的最短路徑要經過中轉點Vi,那么中轉點Vi一定是先于V0被確定了最短路徑的點。我們把點分為兩類,一類是已確定最短路徑的點,稱為“白點”,另一類是未確定最短路徑的點,稱為“藍點”。如果我們要求出一個點的最短路徑,就是把這個點由藍點變為白點。從起點到藍點的最短路徑上的中轉點在這個時刻只能是白點。Dijkstra的算法思想,就是一開始將起點到起點的距離標記為0,而后進行n次循環,每次找出一個到起點距離dis[u]最短的點u,將它從藍點變為白點。隨后枚舉所有的藍點vi,如果以此白點為中轉到達藍點vi的路徑dis[u]+w[u][vi]更短的話,這將它作為vi的“更短路徑”dis[vi](此時還不確定是不是vi的最短路徑)。就這樣,我們每找到一個白點,就嘗試著用它修改其他所有的藍點。中轉點先于終點變成白點,故每一個終點一定能夠被它的最后一個中轉點所修改,而求得最短路徑。123452471162求解順序讓我們對以上這段枯燥的文字做一番模擬,加深理解。算法開始時,作為起點的dis[1]
=
0,其他的點dis[i]
=
0x7fffffff。123452471162第一輪循環找到dis[1]最小,將1變成白點。對所有的藍點做出修改,使得dis[2]=2,dis[3]=4,dis[4]=7。這時dis[2],dis[3],dis[4]被它的最后一個中轉點1修改為了最短路徑。123452471162第二輪循環找到dis[2]最小,將2變成白點。對所有的藍點做出修改,使得dis[3]=3,dis[5]=4。這時dis[3],dis[5]被它們的最后一個中轉點2修改為了最短路徑。123452471162第三輪循環找到dis[3]最小,將3變成白點。對所有的藍點做出修改,使得dis[4]=4。發現以3為中轉不能修改5,說明3不是5的最后一個中轉點。這時dis[4]也被它的最后一個中轉點3修改為了最短路徑。接下來的兩輪循環將4、5也變成白點。N輪循環結束后,所有的點的最短路徑即能求出。Dijkstra無法處理邊權為負的情況,例如右圖這個例子。2到3的邊權值為-4,顯然從起點1到3的最短路徑是-2(1→2→3),但是dijskstra在第二輪循環開始時會找到當前dis[i]最小的點3,并標記它為白點。這時的dis[3]=1,然而1卻不是從起點到點3的最短路徑。因為3已被標記為白點,最短路徑值dis[3]不會再被修改了,所以我們在邊權存在負數的情況下得到了錯誤的答案!12345213-4562【例4-3】、最短路徑問題(Dijkstra)題目參見“Floyed算法”,但本題要求使用dijkstra算法解決。#include<iostream>#include<cstdio>#include<cstring>#include<cmath>usingnamespacestd;inta[101][3];doublec[101];boolb[101];doublef[101][101];intn,i,j,k,x,y,m,s,e;doubleminl;doublemaxx=1e30;intmain(){cin>>n;for(i=1;i<=n;i++)cin>>a[i][1]>>a[i][2];for(i=1;i<=n;i++)for(j=1;j<=n;j++)f[i][j]=maxx;//f數組初始化最大值
cin>>m;for(i=1;i<=m;i++)//預處理x.y間距離f[x][y]{cin>>x>>y;f[x][y]=f[y][x]=sqrt(pow(double(a[x][1]-a[y][1]),2)+pow(double(a[x][2]-a[y][2]),2));}
cin>>s>>e;for(i=1;i<=n;i++)c[i]=f[s][i];memset(b,false,sizeof(b)); //dijkstra最短路
b[s]=true;c[s]=0;for(i=1;i<=n-1;i++){minl=maxx;k=0;for(j=1;j<=n;j++)//查找可以更新的點
if((!b[j])&&(c[j]<minl)){minl=c[j];k=j;}if(k==0)break;b[k]=true;for(j=1;j<=n;j++)
if(c[k]+f[k][j]<c[j])c[j]=c[k]+f[k][j];}printf("%.2lf\n",c[e]);return0;}【例4-4】最小花費【問題描述】
在n個人中,某些人的銀行賬號之間可以互相轉賬。這些人之間轉賬的手續費各不相同。給定這些人之間轉賬時需要從轉賬金額里扣除百分之幾的手續費,請問A最少需要多少錢使得轉賬后B收到100元。【輸入格式】
第一行輸入兩個正整數n,m,分別表示總人數和可以互相轉賬的人的對數。以下m行每行輸入三個正整數x,y,z,表示標號為x的人和標號為y的人之間互相轉賬需要扣除z%的手續費(z<100)。最后一行輸入兩個正整數A,B。數據保證A與B之間可以直接或間接地轉賬。【輸出格式】
輸出A使得B到賬100元最少需要的總費用。精確到小數點后8位。【輸入樣例】3312123213313【輸出樣例】103.07153164【數據規模】1<=n<=2000【參考程序】#include<iostream>usingnamespacestd;doublea[2001][2001],dis[2001]={0},minn;intn,m,i,j,k,x,y,f[2001]={0};voidinit(){cin>>n>>m;for(i=1;i<=m;i++){scanf("%d%d",&j,&k);scanf("%lf",&a[j][k]);a[j][k]=(100-a[j][k])/100;a[k][j]=a[j][k];}cin>>x>>y;}voiddijkstra(intx){for(i=1;i<=n;i++)dis[i]=a[x][i];dis[x]=1;f[x]=1;for(i=1;i<=n-1;i++){minn=0;for(j=1;j<=n;j++)if(f[j]==0&&dis[j]>minn){k=j;minn=dis[j];}f[k]=1;if(k==y)break;for(j=1;j<=n;j++)if(f[j]==0&&dis[k]*a[k][j]>dis[j])dis[j]=dis[k]*a[k][j];}}intmain(){init();dijkstra(x);printf("%0.8f",100/dis[y]);return0;}3.Bellman-Ford算法O(NE)簡稱Ford(福特)算法,同樣是用來計算從一個點到其他所有點的最短路徑的算法,也是一種單源最短路徑算法。能夠處理存在負邊權的情況,但無法處理存在負權回路的情況(下文會有詳細說明)。算法時間復雜度:O(NE),N是頂點數,E是邊數。算法實現:設s為起點,dis[v]即為s到v的最短距離,pre[v]為v前驅。w[j]是邊j的長度,且j連接u、v。初始化:dis[s]=0,dis[v]=∞(v≠s),pre[s]=0For(i=1;i<=n-1;i++)For(j=1;j<=E;j++)
//注意要枚舉所有邊,不能枚舉點。
if(dis[u]+w[j]<dis[v])
//u、v分別是這條邊連接的兩個點。
{dis[v]
=dis[u]
+
w[j];pre[v]
=
u;}算法分析&思想講解:Bellman-Ford算法的思想很簡單。一開始認為起點是白點(dis[1]=0),每一次都枚舉所有的邊,必然會有一些邊,連接著白點和藍點。因此每次都能用所有的白點去修改所有的藍點,每次循環也必然會有至少一個藍點變成白點。在上面這個簡單的模擬中能看到白點的“蔓延”情況。負權回路:雖然Bellman-Ford算法可以求出存在負邊權情況下的最短路徑,卻無法解決存在負權回路的情況。
負權回路是指邊權之和為負數的一條回路,上圖中②-④-⑤-③-②這條回路的邊權之和為-3。在有負權回路的情況下,從1到6的最短路徑是多少?答案是無窮小,因為我們可以繞這條負權回路走無數圈,每走一圈路徑值就減去3,最終達到無窮小。所以說存在負權回路的圖無法求出最短路徑,Bellman-Ford算法可以在有負權回路的情況下輸出錯誤提示。如果在Bellman-Ford算法的兩重循環完成后,還是存在某條邊使得:dis[u]+w<dis[v],則存在負權回路:For每條邊(u,v)If(dis[u]+w<dis[v])
returnFalse如果我們規定每條邊只能走一次,在這個前提下可以求出負權回路的最短路徑。這個問題就留待讀者自己思考(提示:對Floyed做一點小處理)。【例4-5】、最短路徑問題(Bellman-Ford)題目參見“Floyed算法”,要求采用Bellman-Ford算法。#include<iostream>#include<cstdio>#include<cmath>usingnamespacestd;intmain(){doublea[101][3],dis[1001],w[1001],min1;intn,m,x,y,k,f[1001][3],s,t;boolb[101];cin>>n;
for(inti=1;i<=n;i++)
scanf("%lf%lf",&a[i][1],&a[i][2]);cin>>m;for(inti=1;i<=m;i++)
//初始化數組dis,f{
dis[i]=0x7fffffff;
f[i][1]=f[i][2]=0x7fffffff;}for(inti=1;i<=m;i++){scanf("%d%d",&x,&y);f[i][1]=x;f[i][2]=y;w[i]=sqrt(pow(a[x][1]-a[y][1],2)+pow(a[x][2]-a[y][2],2));}cin>>s>>t;dis[s]=0;for(inti=1;i<=n;i++)//算法主體
for(intj=1;j<=m;j++)
{if(dis[f[j][1]]+w[j]<dis[f[j][2]])dis[f[j][2]]=dis[f[j][1]]+w[j];if(dis[f[j][2]]+w[j]<dis[f[j][1]])dis[f[j][1]]=dis[f[j][2]]+w[j];
}printf("%.2f",dis[t]);}4、SPFA算法O(kE)SPFA是Bellman-Ford算法的一種隊列實現,減少了不必要的冗余計算。主要思想是:初始時將起點加入隊列。每次從隊列中取出一個元素,并對所有與它相鄰的點進行修改,若某個相鄰的點修改成功,則將其入隊。直到隊列為空時算法結束。這個算法,簡單的說就是隊列優化的bellman-ford,利用了每個點不會更新次數太多的特點發明的此算法。SPFA在形式上和廣度優先搜索非常類似,不同的是廣度優先搜索中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之后再次被放入隊列,也就是說一個點修改過其它的點之后,過了一段時間可能會獲得更短的路徑,于是再次用來修改其它的點,這樣反復進行下去。算法時間復雜度:O(kE),E是邊數。K是常數,平均值為2。算法實現:dis[i]記錄從起點s到i的最短路徑,w[i][j]記錄連接i,j的邊的長度。pre[v]記錄前趨。team[1..n]為隊列,頭指針head,尾指針tail。
布爾數組exist[1..n]記錄一個點是否現在存在在隊列中。
初始化:dis[s]=0,dis[v]=∞(v≠s),memset(exist,false,sizeof(exist));
起點入隊team[1]=s;head=0;tail=1;exist[s]=true;
do{
1、頭指針向下移一位,取出指向的點u。2、exist[u]=false;已被取出了隊列3、for與u相連的所有點v
//注意不要去枚舉所有點,用數組模擬鄰接表存儲
if(dis[v]>dis[u]+w[u][v])
{dis[v]=dis[u]+w[u][v];pre[v]=u;
if(!exist[v])//隊列中不存在v點,v入隊。
{//尾指針下移一位,v入隊;
exist[v]=true;
}}}while(head<tail);循環隊列:采用循環隊列能夠降低隊列大小,隊列長度只需開到2*n+5即可。例題中的參考程序使用了循環隊列。【例4-6】、香甜的黃油(SweetButter)【問題描述】農夫John發現做出全威斯康辛州最甜的黃油的方法:糖。把糖放在一片牧場上,他知道N(1<=N<=500)只奶牛會過來舔它,這樣就能做出能賣好價錢的超甜黃油。當然,他將付出額外的費用在奶牛上。
農夫John很狡猾。像以前的巴甫洛夫,他知道他可以訓練這些奶牛,讓它們在聽到鈴聲時去一個特定的牧場。他打算將糖放在那里然后下午發出鈴聲,以至他可以在晚上擠奶。農夫John知道每只奶牛都在各自喜歡的牧場(一個牧場不一定只有一頭牛)。給出各頭牛在的牧場和牧場間的路線,找出使所有牛到達的路程和最短的牧場(他將把糖放在那)。
【輸入格式】butter.in第一行:三個數:奶牛數N,牧場數P(2<=P<=800),牧場間道路數C(1<=C<=1450)。第二行到第N+1行:1到N頭奶牛所在的牧場號。第N+2行到第N+C+1行:每行有三個數:相連的牧場A、B,兩牧場間距(1<=D<=255),當然,連接是雙向的。【輸出格式】butter.out一行
輸出奶牛必須行走的最小的距離和.【輸入樣例】345234121135237243345樣例圖形
P2P1@--1--@C1\|\\|\573\|\\|\C3C2@--5--@P3P4【輸出樣例】8//說明:放在4號牧場最優【參考程序】#include<iostream>#include<cstdio>#include<cstring>usingnamespacestd;intn,p,c,i,j,x,y,t,min1,head,tail,tot,u;inta[801][801],b[501],dis[801],w[801][801],team[1601];boolexist[801];intmain(){freopen("butter.in","r",stdin);freopen("butter.out","w",stdout);cin>>n>>p>>c;for(i=1;i<=p;i++){
b[i]=0;
a[i][0]=0;for(j=1;j<=p;j++)w[i][j]=0x7fffffff;}for(i=1;i<=n;i++)cin>>b[i];for(i=1;i<=c;i++)//鄰接矩陣存儲
{cin>>x>>y>>t;w[x][y]=t;a[x][++a[x][0]]=y;a[y][++a[y][0]]=x;w[y][x]=w[x][y];}min1=0x7fffffff;for(i=1;i<=p;i++){for(j=1;j<=p;j++)dis[j]=0x7fffffff;memset(team,0,sizeof(team));//隊列數組初始化memset(exist,false,sizeof(exist));//exist標志初始化dis[i]=0;team[1]=i;head=0;tail=1;exist[i]=true;//起始點入隊do{head++;head=((head-1)%1601)+1;//循環隊列處理u=team[head];exist[u]=false;for(j=1;j<=a[u][0];j++)if(dis[a[u][j]]>dis[u]+w[u][a[u][j]]){dis[a[u][j]]=dis[u]+w[u][a[u][j]];if(!exist[a[u][j]]){tail++;tail=((tail-1)%1601)+1;team[tail]=a[u][j];exist[a[u][j]]=true;}}}while(head!=tail);tot=0;for(j=1;j<=n;j++)
tot+=dis[b[j]];if(tot<min1)min1=tot;}cout<<min1;return0;}二、輸出最短路徑1.單源最短路徑的輸出Dijkstra,Bellman-Ford,SPFA都是單源最短路徑算法,它們的共同點是都有一個數組pre[x]用來記錄從起點到x的最短路徑中,x的前驅結點是哪個。每次更新,我們就給pre[x]賦一個新值,結合上面的思想講解,相信對于記錄某點的前驅結點是不難理解的。那么怎么利用pre[x]數組輸出最短路徑方案呢?【例4-7】、最短路徑問題(輸出路徑)要求改寫程序,用Dijkstra、Bellman-Ford、SPFA算法輸出最短路徑的方案。使用一個小小的遞歸過程就能解決這一問題。voidprint(intx){if(pre[a][x]==0)return;//起點的前驅我們已設為0print(pre[a][x]);
cout<<"->"<<x;}//主程序中:main{……(進行Dijkstra或Bellman-Ford,SPFA運算)cout<<s;print(e);//s是起點,e是終點}2.Floyed算法輸出最短路徑Floyed算法輸出路徑也是采用記錄前驅點的方式。因為floyed是計算任意兩點間最短路徑的算法,dis[i][j]記錄從i到j的最短路徑值。故而我們定義pre[i][j]為一個二維數組,記錄從i到j的最短路徑中,j的前驅點是哪一個。【例4-8】、最短路徑問題(Floyed法輸出路徑)要求改寫Floyed的程序,模仿Dijkstra輸出路徑的方法用floyed輸出最短路徑方案。【參考程序】#include<iostream>#include<cmath>#include<cstring>usingnamespacestd;doubledis[101][101];intx[101],y[101];intpre[101][101];intn,i,j,k,m,a,b;intpf(intx){returnx*x;}voidprint(intx){if(pre[a][x]==0)return;//pre[a][a]=0,說明已經遞歸到起點aprint(pre[a][x]);cout<<"->"<<x;}intmain(){cin>>n;for(i=1;i<=n;i++)cin>>x[i]>>y[i];memset(dis,0x7f,sizeof(dis));
//初始化數組cin>>m;memset(pre,0,sizeof(pre));//初始化前驅數組for(i=1;i<=m;i++){cin>>a>>b;dis[a][b]=dis[b][a]=sqrt(pf(x[a]-x[b])+pf(y[a]-y[b]));pre[a][b]=a;
//a與b相連,自然從a到b的最短路徑b的前驅是apre[b][a]=b;}cin>>a>>b;for(k=1;k<=n;k++)//floyed最短路for
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 基于可信平臺模型的代理系統安全與隱私保護研究-洞察闡釋
- 歷史敘事-解構與重構的敘事學創新-洞察闡釋
- 人教版四年級英語教學方法計劃
- 在線輔導教師師德師風引導計劃
- 人工智能在性能優化中的應用-洞察闡釋
- 學校操場綠化材料準備與質量保障措施
- 洗染行業市場分析-洞察闡釋
- 智能語音交互在公眾娛樂系統中的應用-洞察闡釋
- 德育與學科融合教學計劃
- 巖溶洞穴生態系統的生態修復技術-洞察闡釋
- 2025年天津市河北區中考第一次模擬道德與法治試卷
- 2025風力發電工程安裝合同標準范本
- 化工企業各部門、各崗位處罰細則
- 2025版校園食堂日管控、周排查、月調度記錄表
- DB53-T 1353-2025 歷史遺留冶煉渣堆原位風險管控效果評估 技術指南
- 2025-2030中國X射線和輻射探測器行業市場發展趨勢與前景展望戰略分析研究報告
- 2025年戒毒常識考試題及答案
- 2025年安徽省六安市清水河學校中考一模化學試題(原卷版+解析版)
- 部編版語文三年級下冊第23課《海底世界》精美課件
- 2025年安全教育培訓考試題庫(基礎強化版)應急救援知識試題
- 消防工程施工的重點難點及應對策略
評論
0/150
提交評論