圖論最短路徑問題

圖論最短路徑問題

最最原始的問題——兩點(diǎn)間的最短路
這類背景一般是類似:已知各城市之間距離,請給出從城市A到城市B的最短行車方案 or 各城市距離一致凿滤,給出需要最少中轉(zhuǎn)方案。

深度優(yōu)先搜索(dfs)

  • DFS算法關(guān)鍵思想僅在于解決當(dāng)下該如何做济竹。至于“下一步如何做”則與“當(dāng)下該如何做”是一樣的惋啃,把參數(shù)改為進(jìn)入下一步的值再調(diào)用一下dfs()即可。
  • 而在寫dfs函數(shù)的時(shí)候就只要解決當(dāng)在第step的時(shí)候你該怎么辦毅该,通常就是把每一種可能都去嘗試一遍博秫。當(dāng)前這一步解決后便進(jìn)入下一步dfs(step+1),剩下的事情就不用管它了眶掌。

基本模型:

void dfs(int step)
{
    判斷邊界
    嘗試每一種可能 for(int i = 1; i <= n; i++)
    {
        繼續(xù)下一步 dfs(step+1)
    }
}

廣度優(yōu)先搜索

  • 對于所有邊權(quán)相同的情況挡育,用廣度優(yōu)先搜索會更快更方便。
  • 比如上面提到的最少中轉(zhuǎn)方案問題朴爬,問從城市1到城市4需要經(jīng)過的最少中轉(zhuǎn)城市個(gè)數(shù)即寒。
int bfs()
{
    queue<pair<int,int>> que; //pair記錄城市編號和dis,也可以用結(jié)構(gòu)體
    que.push({1,0}); //把起始點(diǎn)加入隊(duì)列
    book[1] = 1; //標(biāo)記為已在路徑中
    while(!que.empty()) 
    {
        int cur = que.front();
        que.pop();
        for(int i = 1; i <= n; i++)
        {
            if(e[cur][i] != MAX && book[i] == 0) //若從cur到i可達(dá)且i不在隊(duì)列中召噩,i入隊(duì)
            {
                que.push({i, cur.second+1});
                book[i] = 1;
                if(i == n) return cur.second; //如果已擴(kuò)展出目標(biāo)結(jié)點(diǎn)了蒿叠,返回中轉(zhuǎn)城市數(shù)答案
            }
        }
    }
}

Floyd算法

  • 膨脹——任意兩點(diǎn)間的最短路
    • 已經(jīng)知道了求解固定兩點(diǎn)間的最短路,那要怎么求任意兩點(diǎn)間的最短路呢蚣常?
    • 顯然,可以進(jìn)行n^2 次的dfs或bfs輕松搞定(被打)痊银。


  • 觀察會發(fā)現(xiàn)抵蚊,如果要讓兩點(diǎn) i , j 間的路程變短,只能通過第三個(gè)點(diǎn) k 的中轉(zhuǎn)溯革。
  • 比如上面第一張圖贞绳,從 1->5 距離為10,但 1->2->5 距離變成9了致稀。
  • 事實(shí)上冈闭,每個(gè)頂點(diǎn)都有可能使另外兩個(gè)頂點(diǎn)間的路程變短。這種通過中轉(zhuǎn)變短的操作叫做松弛抖单。
  • 當(dāng)任意兩點(diǎn)間不允許經(jīng)過第三個(gè)點(diǎn)時(shí)萎攒,這些城市之間的最短路程就是初始路程:
  • 假如現(xiàn)在允許經(jīng)過1號頂點(diǎn)的中轉(zhuǎn),求任意兩點(diǎn)間的最短路矛绘,這時(shí)候就可以遍歷每一對頂點(diǎn)耍休,試試看通過1號能不能縮短他們的距離。
for(int i = 1; i <= n; i++)
    for(int j = 1; j <= n; j++)
    {
        if(e[i][j] > e[i][1]+e[1][j]) e[i][j] = e[i][1]+e[1][j];
    }
  • 擴(kuò)展一下货矮,先允許1號頂點(diǎn)作為中轉(zhuǎn)給所有兩兩松弛一波羊精,再允許2號、3號...n號都做一遍囚玫,就能得到最終任意兩點(diǎn)間的最短路了喧锦。
  • 這就是Floyd算法读规,雖然時(shí)間復(fù)雜度是令人發(fā)怵的O(n^3),但核心代碼只有五行燃少,實(shí)現(xiàn)起來非常容易束亏。
for(int k = 1; k <= n; k++)
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(e[i][j] > e[i][k]+e[k][j]) 
                e[i][j] = e[i][k]+e[k][j];

單源最短路 Dijkstra

  • 指定源點(diǎn),求它到其余各個(gè)結(jié)點(diǎn)的最短路供汛。
  • 比如給出這張圖枪汪,假設(shè)把1號結(jié)點(diǎn)作為源點(diǎn)。
  • 還是用數(shù)組dis來存1號到其余各點(diǎn)的初始路程:
  • 既然是求最短路徑怔昨,那先選一個(gè)離1號最近的結(jié)點(diǎn)雀久,也就是2號結(jié)點(diǎn)。這時(shí)候趁舀,dis[2]=1 就固定了赖捌,它就是1到2的最短路徑。這是為啥矮烹?因?yàn)槟壳半x1號最近的是2號越庇,且這個(gè)圖的所有邊都是正數(shù),那就不可能能通過第三個(gè)結(jié)點(diǎn)中轉(zhuǎn)使得距離進(jìn)一步縮短了奉狈。因?yàn)閺?號出發(fā)已經(jīng)找不到哪條路比直接到達(dá)2號更短了卤唉。
  • 選好了2號結(jié)點(diǎn),現(xiàn)在看看2號的出邊仁期,有2->3和2->4桑驱。先討論通過2->3這條邊能否讓1號到3號的路程變短,也即比較dis[3]和dis[2]+e[2][3]的大小跛蛋。發(fā)現(xiàn)是可以的熬的,于是dis[3]從12變?yōu)樾碌母搪?0。同理赊级,通過2->4也條邊也更新下dis[4]押框。
  • 松弛完畢后dis數(shù)組變?yōu)椋?/li>
  • 接下來,繼續(xù)在剩下的 3 4 5 6 結(jié)點(diǎn)中選一個(gè)離1號最近的結(jié)點(diǎn)理逊。發(fā)現(xiàn)當(dāng)前是4號離1號最近橡伞,于是dis[4]確定了下來,然后繼續(xù)對4的所有出邊看看能不能做松弛挡鞍。
  • 這樣一直做下去直到已經(jīng)沒有“剩下的”結(jié)點(diǎn)骑歹,算法結(jié)束。
  • 這就是Dijkstra算法墨微,整個(gè)算法的基本步驟是:
    • 所有結(jié)點(diǎn)分為兩部分:已確定最短路的結(jié)點(diǎn)集合P道媚、未知最短路的結(jié)點(diǎn)集合Q。最開始,P中只有源點(diǎn)這一個(gè)結(jié)點(diǎn)最域。(可用一個(gè)book數(shù)組來維護(hù)是否在P中)
    • 在Q中選取一個(gè)離源點(diǎn)最近的結(jié)點(diǎn)u(dis[u]最星捶帧)加入集合P。然后考察u的所有出邊镀脂,做松弛操作牺蹄。
    • 重復(fù)第二步,直到集合Q為空薄翅。最終dis數(shù)組的值就是源點(diǎn)到所有頂點(diǎn)的最短路沙兰。
for(int i = 1; i <= n; i++) dis[i] = e[1][i]; //初始化dis為源點(diǎn)到各點(diǎn)的距離
for(int i = 1; i <= n; i++) book[i] = 0; 
book[1] = 1; //初始時(shí)P集合中只有源點(diǎn)

for(int i = 1; i <= n-1; i++) //做n-1遍就能把Q遍歷空
{
    int min = INF;
    int u;
    for(int j = 1; j <= n; j++) //尋找Q中最近的結(jié)點(diǎn)
    {
        if(book[j] == 0 && dis[j] < min)
        {
            min = dis[j];
            u = j;
        }
    }
    book[u] = 1; //加入到P集合
    for(int v = 1; v <= n; v++) //對u的所有出邊進(jìn)行松弛
    {
        if(e[u][v] < INF) 
        {
            if(dis[v] > dis[u] + e[u][v]) 
                dis[v] = dis[u] + e[u][v];
        }
    }
}

  • Dijkstra是一種基于貪心策略的算法。每次新擴(kuò)展一個(gè)路徑最短的點(diǎn)翘魄,更新與它相鄰的所有點(diǎn)鼎天。當(dāng)所有邊權(quán)為正時(shí),由于不會存在一個(gè)路程更短的沒擴(kuò)展過的點(diǎn)暑竟,所以這個(gè)點(diǎn)的路程就確定下來了斋射,這保證了算法的正確性。
  • 但也正因?yàn)檫@樣但荤,這個(gè)算法不能處理負(fù)權(quán)邊罗岖,因?yàn)閿U(kuò)展到負(fù)權(quán)邊的時(shí)候會產(chǎn)生更短的路徑,有可能破壞了已經(jīng)更新的點(diǎn)路程不會改變的性質(zhì)腹躁。

Bellman-Ford算法

  • 于是桑包,Bellman-Ford算法華麗麗的出場啦。它不僅可以處理負(fù)權(quán)邊纺非,而且算法思想優(yōu)美捡多,且核心代碼只有短短四行。
    (用三個(gè)數(shù)組存邊铐炫,第i條邊表示u[i]->v[i],權(quán)值為w[i])
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];
  • 后兩行代碼的意思是蒜焊,看看能否通過u[i]->v[i]這條邊縮短dis[v[i]]倒信。加上第二行的for,也就是把所有的m條邊一個(gè)個(gè)拎出來泳梆,看看能不能縮短dis[v[i]](松弛)鳖悠。
  • 那把每一條邊松弛一遍后有什么效果呢?
  • 比如求這個(gè)例子:
  • 同樣用dis數(shù)組來存儲1號到各結(jié)點(diǎn)的距離优妙。一開始時(shí)只有dis[1]=0乘综,其他初始化為INF。
  • 先來處理第一條邊 2->3 套硼,然鵝dis[3]是INF卡辰,dis[2]+2也是INF,松弛失敗。
    第二條邊 1->2 九妈,dis[2]是INF反砌,dis[1]-3是-3,松弛成功萌朱,dis[2]更新為-3宴树。
  • 就這樣對所有邊松弛一遍后的結(jié)果如下:
  • 這時(shí)候dis[2]和dis[5]的值變小了,如果再做一輪松弛操作的話晶疼,之前不成功的松弛這時(shí)候也能也就可以起作用了酒贬。
  • 換句話說,第一輪松弛后得到的是從1號出發(fā)“只能經(jīng)過1條邊”到達(dá)其余各點(diǎn)的最短路翠霍,第二輪松弛后得到的是“只能經(jīng)過2條邊”到達(dá)其余各點(diǎn)的最短路锭吨,如果進(jìn)行第k輪松弛得到的就是“只能經(jīng)過k條邊”到達(dá)其余各點(diǎn)的最短路。
  • 那么到底需要進(jìn)行多少輪呢壶运?答案是n-1輪耐齐。因?yàn)樵谝粋€(gè)含有n個(gè)頂點(diǎn)的圖中,任意兩點(diǎn)間的最短路最多包含n-1條邊蒋情。也就解釋了代碼的第一行埠况,是在進(jìn)行n-1輪松弛。
for(int i = 1; i <= n; i++) dis[i] = INF;
dis[1] = 0; //初始化dis數(shù)組棵癣,只有1號的距離為0

for(int k = 1; k <= n-1; k++) //進(jìn)行n-1輪松弛
    for(int i = 1; i <= m; i++) //枚舉每一條邊
        if(dis[v[i]] > dis[u[i]] + w[i]) //嘗試進(jìn)行松弛
            dis[v[i]] = dis[u[i]] + w[i];
  • 此外辕翰,Bellman-Ford算法還可以檢測一個(gè)圖是否含有負(fù)權(quán)回路。如果在進(jìn)行了n-1次松弛之后狈谊,仍然存在某個(gè)dis[v[i]] > dis[u[i]] + w[i]的情況喜命,還可以繼續(xù)成功松弛,那么必然存在回路了(因?yàn)檎碇v最短路徑包含的邊最多只會有n-1條)河劝。
  • 判斷負(fù)權(quán)回路也即在上面那段代碼之后加上一行:
for(int i = 1; i <= m; i++) 
    if(dis[v[i]] > dis[u[i]] + w[i]) flag = 1;
  • Bellman-Ford算法的時(shí)間復(fù)雜度是O(nm)壁榕,貌似比Dijkstra還高。
  • 事實(shí)上還可以進(jìn)行優(yōu)化赎瞎,比如可以加一個(gè)bool變量check用來標(biāo)記數(shù)組dis在本輪松弛中是否發(fā)生了變化牌里,如果沒有,就可以提前跳出循環(huán)务甥。因?yàn)槭恰白疃唷边_(dá)到n-1輪筛峭,實(shí)際情況下經(jīng)常是早就已經(jīng)達(dá)到最短僧界,沒法繼續(xù)成功松弛了藤乙。
for(int k = 1; k <= n-1; k++) //進(jìn)行n-1輪松弛
{
    bool check = 0;
    for(int i = 1; i <= m; i++) //枚舉每一條邊
        if(dis[v[i]] > dis[u[i]] + w[i]) //嘗試進(jìn)行松弛
        {
            dis[v[i]] = dis[u[i]] + w[i];
            check = 1;
        }
    if(check == 0) break;
}  

SPFA算法(隊(duì)列優(yōu)化的Bellman-Ford)

  • 另外一種優(yōu)化是:每次僅對最短路估計(jì)值發(fā)生了變化的結(jié)點(diǎn)的所有出邊進(jìn)行松弛操作外盯。因?yàn)樵谏厦娴乃惴ㄖ校繉?shí)施一次松弛操作后挺尿,就會有一些頂點(diǎn)已經(jīng)求得最短路之后便不會再改變了(由估計(jì)值變?yōu)榇_定值)奏黑,既然都已經(jīng)不受后續(xù)松弛操作的影響了卻還是每次都要判斷是否需要松弛炊邦,就浪費(fèi)了時(shí)間。
  • 可以用隊(duì)列來維護(hù)dis發(fā)生了變化的那些結(jié)點(diǎn)攀涵。具體操作是:
    • 初始時(shí)將源點(diǎn)加入隊(duì)列铣耘。
    • 每次選取隊(duì)首結(jié)點(diǎn)u,對u的所有出邊進(jìn)行松弛以故。假設(shè)有一條邊u->v松弛成功了蜗细,那就把v加入隊(duì)列。然而怒详,同一個(gè)結(jié)點(diǎn)同時(shí)在隊(duì)列中出現(xiàn)多次是毫無意義的(可以用一個(gè)bool數(shù)組來判哪些結(jié)點(diǎn)在隊(duì)列中)炉媒。所以剛提到的操作其實(shí)是,如果v不在當(dāng)前隊(duì)列中昆烁,才把它加入隊(duì)列吊骤。
    • 對u的所有出邊松弛完畢后,u出隊(duì)静尼。接下來不斷的取出新的隊(duì)首做第2步操作白粉,直到隊(duì)列為空。
  • 一個(gè)例子:
  • 用數(shù)組dis來存放1號結(jié)點(diǎn)到各點(diǎn)的最短路鼠渺。初始時(shí)dis[1]為0鸭巴。接下來將1號結(jié)點(diǎn)入隊(duì)。
  • 現(xiàn)在看1號的所有出邊拦盹,對于1->2鹃祖,比較dis[2]和dis[1]+e[1][2]的大小,發(fā)現(xiàn)松弛成功普舆,dis[2]從INF變?yōu)?恬口。并且2不在隊(duì)列中,所以2號結(jié)點(diǎn)入隊(duì)沼侣。同理祖能,5號結(jié)點(diǎn)也松弛成功,入隊(duì)蛾洛。
  • 1號結(jié)點(diǎn)處理完畢芯杀,此時(shí)將1號出隊(duì),接著對隊(duì)首也就是2號結(jié)點(diǎn)進(jìn)行同樣的處理雅潭。在處理2->5這條邊的時(shí)候,雖然松弛成功却特,dis[5]從10更新為9了扶供,但5號頂點(diǎn)已經(jīng)在隊(duì)列中,所以5號不能再次入隊(duì)裂明。
  • 處理完2號之后就長這樣:
  • 接著一直持續(xù)下去椿浓,直到隊(duì)列為空,算法結(jié)束。
for(int i = 1; i <= n; i++) book[i] = 0; //初始時(shí)都不在隊(duì)列中
queue<int> que;
que.push(1); //將結(jié)點(diǎn)1加入隊(duì)列
book[1] = 1; //并打標(biāo)記

while(!que.empty())
{
    int cur = que.empty(); //取出隊(duì)首
    for(int i = 1; i <= n; i++) 
    {
        if(e[cur][i] != INF && dis[i] > dis[cur]+e[cur][i]) //若cur到i有邊且能夠松弛
        {
            dis[i] = dis[cur]+e[cur][i]; //更新dis[i]
            if(book[i] == 0) //若i不在隊(duì)列中則加入隊(duì)列
            {
                que.push(i);
                book[i] = 1;
            }
        }
    }
    
    que.pop(); //隊(duì)首出隊(duì)
    book[cur] = 0;
}
  • 這其實(shí)就是SPFA算法(隊(duì)列優(yōu)化的Bellman-Ford)扳碍,它的關(guān)鍵思想就在于:只有那些在前一遍松弛中改變了最短路估計(jì)值的結(jié)點(diǎn)提岔,才可能引起它們鄰接點(diǎn)最短路估計(jì)值發(fā)生改變。
  • 它也能夠判斷負(fù)權(quán)回路:如果某個(gè)點(diǎn)進(jìn)入隊(duì)列的次數(shù)超過n次笋敞,則存在負(fù)環(huán)碱蒙。

最短路徑算法的對比

Reference

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夯巷,一起剝皮案震驚了整個(gè)濱河市赛惩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趁餐,老刑警劉巖喷兼,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異后雷,居然都是意外死亡季惯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門臀突,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勉抓,“玉大人,你說我怎么就攤上這事惧辈×兆矗” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵盒齿,是天一觀的道長念逞。 經(jīng)常有香客問我,道長边翁,這世上最難降的妖魔是什么翎承? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮符匾,結(jié)果婚禮上叨咖,老公的妹妹穿的比我還像新娘。我一直安慰自己啊胶,他們只是感情好甸各,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著焰坪,像睡著了一般趣倾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上某饰,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天儒恋,我揣著相機(jī)與錄音善绎,去河邊找鬼。 笑死诫尽,一個(gè)胖子當(dāng)著我的面吹牛禀酱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播牧嫉,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼剂跟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驹止?” 一聲冷哼從身側(cè)響起浩聋,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎臊恋,沒想到半個(gè)月后衣洁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抖仅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年坊夫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撤卢。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡环凿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出放吩,到底是詐尸還是另有隱情智听,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布渡紫,位于F島的核電站到推,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惕澎。R本人自食惡果不足惜莉测,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唧喉。 院中可真熱鬧捣卤,春花似錦、人聲如沸八孝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽干跛。三九已至子姜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驯鳖,已是汗流浹背闲询。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浅辙,地道東北人扭弧。 一個(gè)月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像记舆,于是被迫代替她去往敵國和親鸽捻。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361