【数据结构与算法拓展】最短路径的方案数统计问题(基于floyd算法)
前言
數據結構,一門數據處理的藝術,精巧的結構在一個又一個算法下發揮著他們無與倫比的高效和精密之美,在為信息技術打下堅實地基的同時,也令無數開發者和探索者為之著迷。
也因如此,它作為博主大二上學期最重要的必修課出現了。由于大家對于上學期C++系列博文的支持,我打算將這門課的筆記也寫作系列博文,既用于整理、消化,也用于同各位交流、展示數據結構的美。
此系列文章,將會分成兩條主線,一條“數據結構基礎”,一條“數據結構拓展”?!皵祿Y構基礎”主要以記錄課上內容為主,“拓展”則是以課上內容為基礎的更加高深的數據結構或相關應用知識。
歡迎關注博主,一起交流、學習、進步,往期的文章將會放在文末。
隨著學習的深入,相信各位讀者的實力已經可以對付計算出任意兩點間最短路徑的問題了。那么更進一步的,在這一節我們要討論關于最短路徑的計數問題。在計算出任意兩點間的最短路徑長度的基礎上,我們還需要統計出這些最短路徑的方案數。
這個問題,乍一看感覺有些不同尋常,因為不管怎么說,方案數相較于長度來說總還是不好把握的。我們在思考統計方案數的算法的時候總是容易囿于對起點到終點路徑規劃決策的模擬。
樸素的解決思路多是先在紙上推演從一起點到終點的路徑,一條一條的畫出最短路徑。小規模的時候,找出答案不是難事,但是一旦數據量較大,筆算就會漏洞百出,更何況一旦準備將其付諸于程序,就像是秀才遇到兵,發現根本無從下手。。。。
所以解決問題的關鍵就是要跳出樸素的思維方式,用一種新的視角來審視這個問題。不妨先從回顧計算最短路徑長度的算法開始。
floyd算法求最短路徑
任意兩點間的最短路徑問題,解決的方案是選擇不停地枚舉中間點來對路徑進行“松弛”,讓各點之間不斷挑選更優的路徑從而將距離邊的更短。
如下圖,對于點對14來說,可以經由中間點2,走1->2->4這條更短的路徑。
當然,能用來更新最短路徑的中間點也可以不是直接相連的頂點,例如下圖:
點對15的最短路徑可能由3更新成為5。不過在此之前,13和35的距離應該分別被2和4更新成為2.
可以看到,在這個問題中,有個關鍵的狀態定義,點對(i,j)(i,j)(i,j)之間的最短路徑長度dijd_{ij}dij?。
這個定義讓我們只關心點與點之間的路徑長度關系而非具體路徑。
進一步的,根據這個狀態定義,可以找到狀態間轉移方案:經過中間點k的點對ij的最短路徑就是:
dij=min(dik+dkj,dij)d_{ij}=min(d_{ik}+d_{kj},d_{ij}) dij?=min(dik?+dkj?,dij?)
那么在算法的初始,將最短路徑長度初始化:
dii=0d_{ii}=0dii?=0
dij=inf(ij之間沒有邊相連)d_{ij}=inf\ (ij之間沒有邊相連)dij?=inf?(ij之間沒有邊相連)
dij=lenij(ij之間有長度為lenij的邊鏈接)d_{ij}=len_{ij}\ (ij之間有長度為len_{ij}的邊鏈接)dij?=lenij??(ij之間有長度為lenij?的邊鏈接)
于是這個最短路徑長度的算法問題就可以解了,步驟如下:
- 依次枚舉所有中間節點k,更新其余點對
- 枚舉非中間節點的點對ij
- 使用中間節點k對ij之間的最短路徑長度進行松弛
代碼實現如下:
int dis[N][N];//最短路徑數組,ij最短路徑為dis[i][j] void floyd(int n){for(int k = 1;k <= n;k++){//枚舉中間節點for(int i = 1;i <= n;i++){//枚舉非中間節點的點對if(i == k)continue;for(int j = i + 1;j <= n;j++){if(j == k)continue;if(dis[i][j] > dis[i][k] + dis[k][j]){//松弛最短路徑dis[i][j] = dis[i][k] + dis[k][j];dis[j][i] = dis[i][k] + dis[k][j];}}}} }兩點間最短路徑方案數
要統計最短路徑的方案數,沒有最短路徑長度肯定是不行的,所以以下的算法要建立在floyd算法之上。也就是說,方案數問題要在floyd的過程中順手解決
前面說過,要解決最短路徑的方案數統計問題。我們要跳出樸素的思維方案,借鑒最短路徑長度的建模分析方法。
不知各位是否注意到了上文在分析最短路徑算法時標記出了三個模塊,他們分別是:
狀態、轉移、初始化狀態、轉移、初始化狀態、轉移、初始化這是解決路徑長度問題的關鍵所在,也就是常常提到floyd用到的動態規劃思想。
現在我們可以從動態規劃的角度借助這三個模塊依葫蘆畫瓢來分析最短路徑方案數的問題。(當然這里對狀態和轉移的描述未必在學術上嚴謹,主要是便于分析解決問題。)
定狀態
依照最短路徑的狀態dijd_{ij}dij?,我們定義:
點對(i,j)之間的最短路徑方案數=cij點對(i,j)之間的最短路徑方案數=c_{ij} 點對(i,j)之間的最短路徑方案數=cij?(c取自count單詞首字母)
有了這個定義,在任意兩點間最短路方案數方面,用一個狀態便可以忽略具體的路徑方案,將方案數量一言以蔽之。
找轉移
定義了狀態之后,我們迫切的想知道在使用中間點k松弛點對ij時,最短路徑的方案數狀態會如何轉移。
首先一點,路徑的計算應該滿足乘法原理,即:
點對(i,j)經過點k的最短路徑方案數=cik×cjk點對(i,j)經過點k的最短路徑方案數=c_{ik}\times c_{jk} 點對(i,j)經過點k的最短路徑方案數=cik?×cjk?
舉個例子,如下圖:
對于中間節點4,點對17之間的最短路徑方案數為2×2=42\times2=42×2=4(14方案數為2,47方案數為2)
乘法原理這里很好理解,就不再贅述了。下面的問題就是在進行floyd算法過程中,計算出來的方案數要怎么進行轉移,這個問題要拆成三種情況來討論:
于是,狀態轉移的問題也就解決了
初始化
方案數的初始化方法就是給每個有邊的點對之間只能通過這條邊到達,方案數置1,否則不能到達,置0
實現
有了上面的鋪墊,下面就可以大刀闊斧的開始計算最短路徑的方案數了。
步驟如下:
- 初始化方案數矩陣
- 執行floyd算法,在松弛過程中同時轉移方案數
- 當經過k不能使得i到j已知路徑更短,即dij<dik+dkjd_{ij} < d_{ik}+d_{kj}dij?<dik?+dkj?,此次松弛無效,不改變方案數cijc_{ij}cij?
- 當經過k的最短路徑長度等于已知路徑長度,即dij=dik+dkjd_{ij} = d_{ik}+d_{kj}dij?=dik?+dkj?,i到j的方案應該加上從k走的方案數,即cij=cij+cik×ckjc_{ij}=c_{ij}+c_{ik}\times c_{kj}cij?=cij?+cik?×ckj?
- 當經過k的最短路徑長度小于已知路徑長度,即dij>dik+dkjd_{ij} > d_{ik}+d_{kj}dij?>dik?+dkj?,i到j之前的方案應該作廢而只算從k走的方案數,即cij=cik×ckjc_{ij}=c_{ik}\times c_{kj}cij?=cik?×ckj?
實現起來也不難,改動一下上文的代碼:
int dis[N][N];//最短路徑長度矩陣 int cnt[N][N];//最短路徑計數矩陣 void floyd(int n){for(int k = 1;k <= n;k++){//枚舉中間節點for(int i = 1;i <= n;i++){//枚舉非中間節點點對if(i == k)continue;for(int j = i + 1;j <= n;j++){if(j == k)continue;if(dis[i][j] == dis[i][k] + dis[k][j]){//當經過中間點最短距離相等,方案數相加cnt[i][j] += cnt[i][k] * cnt[k][j];cnt[j][i] += cnt[i][k] * cnt[k][j];}else if(dis[i][j] > dis[i][k] + dis[k][j]){//當經過中間點最短距離更小,之前方案數歸零,計經過k的方案數dis[i][j] = dis[i][k] + dis[k][j];dis[j][i] = dis[i][k] + dis[k][j];cnt[i][j] = cnt[i][k] * cnt[k][j];cnt[j][i] = cnt[i][k] * cnt[k][j];}}}} }讓我們來擬定一個需求,測試這個場景:
給定n個頂點和m條邊,求出任意兩點間的最短路徑長度和方案數
#include<iostream> using namespace std; int dis[105][105]; int cnt[105][105]; const int inf = 1 << 29;void floyd(int n){for(int k = 1;k <= n;k++){//枚舉中間節點for(int i = 1;i <= n;i++){//枚舉非中間節點點對if(i == k)continue;for(int j = i + 1;j <= n;j++){if(j == k)continue;if(dis[i][j] == dis[i][k] + dis[k][j]){//當經過中間點最短距離相等,方案數相加cnt[i][j] += cnt[i][k] * cnt[k][j];cnt[j][i] += cnt[i][k] * cnt[k][j];}else if(dis[i][j] > dis[i][k] + dis[k][j]){//當經過中間點最短距離更小,之前方案數歸零,計經過k的方案數dis[i][j] = dis[i][k] + dis[k][j];dis[j][i] = dis[i][k] + dis[k][j];cnt[i][j] = cnt[i][k] * cnt[k][j];cnt[j][i] = cnt[i][k] * cnt[k][j];}}}} }int main(){int n,m;scanf("%d%d",&n,&m);for(int i = 1;i <= n;i++){//初始化距離和計數矩陣for(int j = 1;j <= n;j++){dis[i][j] = inf;cnt[i][j] = 0;}dis[i][i] = 0;}for(int i = 0,x,y,l;i < m;i++){//讀入m條邊scanf("%d%d%d",&x,&y,&l);dis[x][y] = l;dis[y][x] = l;cnt[x][y] = 1;cnt[y][x] = 1;}floyd(n);//floyd算法計算長度和計數/*打印長度信息和計數信息*/printf("最短路徑長度矩陣:\n");for(int i = 1;i <= n;i++){for(int j = 1;j <= n;j++){printf("%-3d",dis[i][j] == inf ? -1 : dis[i][j]);}printf("\n");}printf("最短路徑計數矩陣:\n");for(int i = 1;i <= n;i++){for(int j = 1;j <= n;j++){printf("%-3d",cnt[i][j]);}printf("\n");} }進行一個測試樣例:
經過指定頂點的最短路徑數
好的,現在經過改進過的floyd算法,我們可以統計出任意兩點間的最短路徑數量了。那么接下來我們還想知道另一個問題:如何統計兩點間經過指定中間頂點的最短路徑方案數
其實有了前面的鋪墊,這個問題就變得很簡單,我們模仿上面的套路,不妨先給個狀態定義:
點對(i,j)之間經過中間頂點k的最短路徑方案數=cijk點對(i,j)之間經過中間頂點k的最短路徑方案數=c_{ij}^k 點對(i,j)之間經過中間頂點k的最短路徑方案數=cijk?
那么狀態的轉移如下:
- 當ij之間存在經過k的最短路徑,有dij=dik+dkjd_{ij} =d_{ik}+d_{kj}dij?=dik?+dkj?,方案數cijk=cik×ckjc_{ij}^k=c_{ik}\times c_{kj}cijk?=cik?×ckj?(還記的上文的乘法原理嗎?)
- 當ij之間不存在經過k的最短路徑,即dij<dik+dkjd_{ij} <d_{ik}+d_{kj}dij?<dik?+dkj?,方案數cijk=0c_{ij}^k=0cijk?=0
于是乎這個問題就解決了~
往期博客
- 【數據結構基礎】數據結構基礎概念
- 【數據結構基礎】線性數據結構——線性表概念 及 數組的封裝
- 【數據結構基礎】線性數據結構——三種鏈表的總結及封裝
- 【數據結構基礎】線性數據結構——棧和隊列的總結及封裝(C和java)
- 【算法與數據結構基礎】模式匹配問題與KMP算法
- 【數據結構與算法基礎】二叉樹與其遍歷序列的互化 附代碼實現(C和java)
- 【數據結構與算法拓展】 單調隊列原理及代碼實現
- 【數據結構基礎】圖的存儲結構
- 【數據結構與算法基礎】并查集原理、封裝實現及例題解析(C和java)
- 【數據結構與算法拓展】二叉堆原理、實現與例題(C和java)
- 【數據結構與算法基礎】哈夫曼樹與哈夫曼編碼(C++)
- 【數據結構與算法基礎】最短路徑問題
- 【數據結構與算法基礎】堆排序原理及實現
- 【數據結構與算法基礎】最小生成樹算法原理及實現
- 【數據結構基礎】圖的遍歷方法與應用
- 【數據結構基礎】矩陣的存儲結構,數組,三元組表及十字鏈表
- 【數據結構與算法基礎】拓撲排序與AOV網絡
- 【數據結構與算法基礎】AOE網絡與關鍵路徑
- 【數據結構與算法基礎】樹與二叉樹的互化
參考資料:
- 《數據結構》(劉大有,楊博等編著)
- 《算法導論》(托馬斯·科爾曼等編著)
- 《圖解數據結構——使用Java》(胡昭民著)
- OI WiKi
總結
以上是生活随笔為你收集整理的【数据结构与算法拓展】最短路径的方案数统计问题(基于floyd算法)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Sharding-JDBC教程-3:Sp
- 下一篇: 4-Mybatis配置详解