东哥带你刷图论第四期:二分图的判定
學算法認準 labuladong
點擊卡片可搜索關鍵詞👇
讀完本文,可以去力扣解決如下題目:
785. 判斷二分圖(中等)
886. 可能的二分法(中等)
我之前寫了好幾篇圖論相關的文章:
東哥帶你刷圖論第一期:圖遍歷算法
東哥帶你刷圖論第二期:環檢測和拓撲排序
東哥帶你刷圖論第三期:Dijkstra 最短路徑算法
除此之外,并查集算法計算連通分量名流問題?也和圖結構有一些相關性。
那么今天繼續來講一個經典圖論算法:二分圖判定算法。
二分圖簡介
在講二分圖的判定算法之前,我們先來看下百度百科對「二分圖」的定義:
二分圖的頂點集可分割為兩個互不相交的子集,圖中每條邊依附的兩個頂點都分屬于這兩個子集,且兩個子集內的頂點不相鄰。
其實圖論里面很多術語的定義都比較拗口,不容易理解。我們甭看這個死板的定義了,來玩個游戲吧:
給你一幅「圖」,請你用兩種顏色將圖中的所有頂點著色,且使得任意一條邊的兩個端點的顏色都不相同,你能做到嗎?
這就是圖的「雙色問題」,其實這個問題就等同于二分圖的判定問題,如果你能夠成功地將圖染色,那么這幅圖就是一幅二分圖,反之則不是:
在具體講解二分圖判定算法之前,我們先來說說計算機大佬們閑著無聊解決雙色問題的目的是什么。
首先,二分圖作為一種特殊的圖模型,會被很多高級圖算法(比如最大流算法)用到,不過這些高級算法我們不是特別有必要去掌握,有興趣的讀者可以自行搜索。
從簡單實用的角度來看,二分圖結構在某些場景可以更高效地存儲數據。
比如前文 介紹《算法 4》
如果用哈希表存儲,需要兩個哈希表分別存儲「每個演員到電影列表」的映射和「每部電影到演員列表」的映射。
但如果用「圖」結構存儲,將電影和參演的演員連接,很自然地就成為了一幅二分圖:
每個電影節點的相鄰節點就是參演該電影的所有演員,每個演員的相鄰節點就是該演員參演過的所有電影,非常方便直觀。
類比這個例子,其實生活中不少實體的關系都能自然地形成二分圖結構,所以在某些場景下圖結構也可以作為存儲鍵值對的數據結構(符號表)。
好了,接下來進入正題,說說如何判定一幅圖是否是二分圖。
二分圖判定思路
判定二分圖的算法很簡單,就是用代碼解決「雙色問題」。
說白了就是遍歷一遍圖,一邊遍歷一遍染色,看看能不能用兩種顏色給所有節點染色,且相鄰節點的顏色都不相同。
既然說到遍歷圖,也不涉及最短路徑之類的,當然是 DFS 算法和 BFS 皆可了,DFS 算法相對更常用些,所以我們先來看看如何用 DFS 算法判定雙色圖。
首先,基于 學習數據結構和算法的框架思維
/*?二叉樹遍歷框架?*/ void?traverse(TreeNode?root)?{if?(root?==?null)?return;traverse(root.left);traverse(root.right); }/*?多叉樹遍歷框架?*/ void?traverse(Node?root)?{if?(root?==?null)?return;for?(Node?child?:?root.children)traverse(child); }/*?圖遍歷框架?*/ boolean[]?visited; void?traverse(Graph?graph,?int?v)?{//?防止走回頭路進入死循環if?(visited[v])?return;//?前序遍歷位置,標記節點?v?已訪問visited[v]?=?true;for?(TreeNode?neighbor?:?graph.neighbors(v))traverse(graph,?neighbor); }因為圖中可能存在環,所以用visited數組防止走回頭路。
這里可以看到我習慣把 return 語句都放在函數開頭,因為一般 return 語句都是 base case,集中放在一起可以讓算法結構更清晰。
其實,如果你愿意,也可以把 if 判斷放到其它地方,比如圖遍歷框架可以稍微改改:
/*?圖遍歷框架?*/ boolean[]?visited; void?traverse(Graph?graph,?int?v)?{//?前序遍歷位置,標記節點?v?已訪問visited[v]?=?true;for?(int?neighbor?:?graph.neighbors(v))?{if?(!visited[neighbor])?{//?只遍歷沒標記過的相鄰節點traverse(graph,?neighbor);}} }這種寫法把對visited的判斷放到遞歸調用之前,和之前的寫法唯一的不同就是,你需要保證調用traverse(v)的時候,visited[v] == false。
為什么要特別說這種寫法呢?因為我們判斷二分圖的算法會用到這種寫法。
回顧一下二分圖怎么判斷,其實就是讓traverse函數一邊遍歷節點,一邊給節點染色,嘗試讓每對相鄰節點的顏色都不一樣。
所以,判定二分圖的代碼邏輯可以這樣寫:
/*?圖遍歷框架?*/ void?traverse(Graph?graph,?boolean[]?visited,?int?v)?{visited[v]?=?true;//?遍歷節點?v?的所有相鄰節點?neighborfor?(int?neighbor?:?graph.neighbors(v))?{if?(!visited[neighbor])?{//?相鄰節點?neighbor?沒有被訪問過//?那么應該給節點?neighbor?涂上和節點?v?不同的顏色traverse(graph,?visited,?neighbor);}?else?{//?相鄰節點?neighbor?已經被訪問過//?那么應該比較節點?neighbor?和節點?v?的顏色//?若相同,則此圖不是二分圖}} }如果你能看懂上面這段代碼,就能寫出二分圖判定的具體代碼了,接下來看兩道具體的算法題來實操一下。
題目實踐
力扣第 785 題「判斷二分圖」就是原題,題目給你輸入一個 鄰接表 表示一幅無向圖,請你判斷這幅圖是否是二分圖。
函數簽名如下:
boolean?isBipartite(int[][]?graph);比如題目給的例子,輸入的鄰接表graph = [[1,2,3],[0,2],[0,1,3],[0,2]],也就是這樣一幅圖:
顯然無法對節點著色使得每兩個相鄰節點的顏色都不相同,所以算法返回 false。
但如果輸入graph = [[1,3],[0,2],[1,3],[0,2]],也就是這樣一幅圖:
如果把節點{0, 2}涂一個顏色,節點{1, 3}涂另一個顏色,就可以解決「雙色問題」,所以這是一幅二分圖,算法返回 true。
結合之前的代碼框架,我們可以額外使用一個color數組來記錄每個節點的顏色,從而寫出解法代碼:
//?記錄圖是否符合二分圖性質 private?boolean?ok?=?true; //?記錄圖中節點的顏色,false?和?true?代表兩種不同顏色 private?boolean[]?color; //?記錄圖中節點是否被訪問過 private?boolean[]?visited;//?主函數,輸入鄰接表,判斷是否是二分圖 public?boolean?isBipartite(int[][]?graph)?{int?n?=?graph.length;color?=??new?boolean[n];visited?=??new?boolean[n];//?因為圖不一定是聯通的,可能存在多個子圖//?所以要把每個節點都作為起點進行一次遍歷//?如果發現任何一個子圖不是二分圖,整幅圖都不算二分圖for?(int?v?=?0;?v?<?n;?v++)?{if?(!visited[v])?{traverse(graph,?v);}}return?ok; }//?DFS?遍歷框架 private?void?traverse(int[][]?graph,?int?v)?{//?如果已經確定不是二分圖了,就不用浪費時間再遞歸遍歷了if?(!ok)?return;visited[v]?=?true;for?(int?w?:?graph[v])?{if?(!visited[w])?{//?相鄰節點?w?沒有被訪問過//?那么應該給節點?w?涂上和節點?v?不同的顏色color[w]?=?!color[v];//?繼續遍歷?wtraverse(graph,?w);}?else?{//?相鄰節點?w?已經被訪問過//?根據?v?和?w?的顏色判斷是否是二分圖if?(color[w]?==?color[v])?{//?若相同,則此圖不是二分圖ok?=?false;}}} }這就是解決「雙色問題」的代碼,如果能成功對整幅圖染色,則說明這是一幅二分圖,否則就不是二分圖。
接下來看一下 BFS 算法的邏輯:
//?記錄圖是否符合二分圖性質 private?boolean?ok?=?true; //?記錄圖中節點的顏色,false?和?true?代表兩種不同顏色 private?boolean[]?color; //?記錄圖中節點是否被訪問過 private?boolean[]?visited;public?boolean?isBipartite(int[][]?graph)?{int?n?=?graph.length;color?=??new?boolean[n];visited?=??new?boolean[n];for?(int?v?=?0;?v?<?n;?v++)?{if?(!visited[v])?{//?改為使用?BFS?函數bfs(graph,?v);}}return?ok; }//?從?start?節點開始進行?BFS?遍歷 private?void?bfs(int[][]?graph,?int?start)?{Queue<Integer>?q?=?new?LinkedList<>();visited[start]?=?true;q.offer(start);while?(!q.isEmpty()?&&?ok)?{int?v?=?q.poll();//?從節點?v?向所有相鄰節點擴散for?(int?w?:?graph[v])?{if?(!visited[w])?{//?相鄰節點?w?沒有被訪問過//?那么應該給節點?w?涂上和節點?v?不同的顏色color[w]?=?!color[v];//?標記?w?節點,并放入隊列visited[w]?=?true;q.offer(w);}?else?{//?相鄰節點?w?已經被訪問過//?根據?v?和?w?的顏色判斷是否是二分圖if?(color[w]?==?color[v])?{//?若相同,則此圖不是二分圖ok?=?false;}}}} }核心邏輯和剛才實現的traverse函數(DFS 算法)完全一樣,也是根據相鄰節點v和w的顏色來進行判斷的。關于 BFS 算法框架的探討,詳見前文 BFS 算法框架Dijkstra 算法模板
最后再來看看力扣第 886 題「可能的二分法」:
函數簽名如下:
boolean?possibleBipartition(int?n,?int[][]?dislikes);其實這題考察的就是二分圖的判定:
如果你把每個人看做圖中的節點,相互討厭的關系看做圖中的邊,那么dislikes數組就可以構成一幅圖;
又因為題目說互相討厭的人不能放在同一組里,相當于圖中的所有相鄰節點都要放進兩個不同的組;
那就回到了「雙色問題」,如果能夠用兩種顏色著色所有節點,且相鄰節點顏色都不同,那么你按照顏色把這些節點分成兩組不就行了嘛。
所以解法就出來了,我們把dislikes構造成一幅圖,然后執行二分圖的判定算法即可:
private?boolean?ok?=?true; private?boolean[]?color; private?boolean[]?visited;public?boolean?possibleBipartition(int?n,?int[][]?dislikes)?{//?圖節點編號為?1...ncolor?=?new?boolean[n?+?1];visited?=?new?boolean[n?+?1];//?轉化成鄰接表表示圖結構List<Integer>[]?graph?=?buildGraph(n,?dislikes);for?(int?v?=?1;?v?<=?n;?v++)?{if?(!visited[v])?{traverse(graph,?v);}}return?ok; }//?建圖函數 private?List<Integer>[]?buildGraph(int?n,?int[][]?dislikes)?{//?圖節點編號為?1...nList<Integer>[]?graph?=?new?LinkedList[n?+?1];for?(int?i?=?1;?i?<=?n;?i++)?{graph[i]?=?new?LinkedList<>();}for?(int[]?edge?:?dislikes)?{int?v?=?edge[1];int?w?=?edge[0];//?「無向圖」相當于「雙向圖」//?v?->?wgraph[v].add(w);//?w?->?vgraph[w].add(v);}return?graph; }//?和之前的?traverse?函數完全相同 private?void?traverse(List<Integer>[]?graph,?int?v)?{if?(!ok)?return;visited[v]?=?true;for?(int?w?:?graph[v])?{if?(!visited[w])?{color[w]?=?!color[v];traverse(graph,?w);}?else?{if?(color[w]?==?color[v])?{ok?=?false;}}} }至此,這道題也使用 DFS 算法解決了,如果你想用 BFS 算法,和之前寫的解法是完全一樣的,可以自己嘗試實現。
二分圖的判定算法就講到這里,更多二分圖的高級算法,敬請期待。
最后,公眾號后臺回復「微信」可加我好友,回復「目錄」可獲取精選文章分類。關注我的視頻號,每周直播分享:
總結
以上是生活随笔為你收集整理的东哥带你刷图论第四期:二分图的判定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 论文阅读笔记:DOER: Dual Cr
- 下一篇: 2020美国纽约大学计算机科学排名,纽约