日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當前位置:首頁 > 單片機 > 程序員小灰
[導讀]讀完本文,可以去力扣解決如下題目:207.課程表(Medium)210.課程表II(Medium)很多讀者留言說要看「圖」相關(guān)的算法,那就滿足大家,結(jié)合算法題把圖相關(guān)的技巧給大家過一遍。數(shù)據(jù)結(jié)構(gòu)相關(guān)的算法無非兩點:遍歷訪問。那么圖的基本遍歷方法也很簡單,以前就講過如何從多叉樹的遍...

讀完本文,可以去力扣解決如下題目:207.課程表(Medium

210.課程表 II(Medium

很多讀者留言說要看「圖」相關(guān)的算法,那就滿足大家,結(jié)合算法題把圖相關(guān)的技巧給大家過一遍。

數(shù)據(jù)結(jié)構(gòu)相關(guān)的算法無非兩點:遍歷 訪問。那么圖的基本遍歷方法也很簡單,以前就講過如何從多叉樹的遍歷框架擴展到圖的遍歷。

圖這種數(shù)據(jù)結(jié)構(gòu)還有一些比較特殊的算法,比如二分圖判斷,有環(huán)圖無環(huán)圖的判斷,拓撲排序,以及最經(jīng)典的最小生成樹,單源最短路徑問題,更難的就是類似網(wǎng)絡流這樣的問題。

不過以我的經(jīng)驗呢,像網(wǎng)絡流這種問題,你又不是打競賽的,除非自己特別有興趣,否則就沒必要學了;像最小生成樹和最短路徑問題,雖然從刷題的角度用到的不多,但它們屬于經(jīng)典算法,學有余力可以掌握一下;像拓撲排序這一類,屬于比較基本且有用的算法,應該比較熟練地掌握。

那么本文就結(jié)合具體的算法題,來說說拓撲排序算法原理,因為拓撲排序的對象是有向無環(huán)圖,所以順帶說一下如何判斷圖是否有環(huán)。

判斷有向圖是否存在環(huán)

先來看看力扣第 207 題「課程表」:

函數(shù)簽名如下:

int[]?findOrder(int?numCourses,?int[][]?prerequisites);
題目應該不難理解,什么時候無法修完所有課程?當存在循環(huán)依賴的時候。

其實這種場景在現(xiàn)實生活中也十分常見,比如我們寫代碼 import 包也是一個例子,必須合理設計代碼目錄結(jié)構(gòu),否則會出現(xiàn)循環(huán)依賴,編譯器會報錯,所以編譯器實際上也使用了類似算法來判斷你的代碼是否能夠成功編譯。

看到依賴問題,首先想到的就是把問題轉(zhuǎn)化成「有向圖」這種數(shù)據(jù)結(jié)構(gòu),只要圖中存在環(huán),那就說明存在循環(huán)依賴。

具體來說,我們首先可以把課程看成「有向圖」中的節(jié)點,節(jié)點編號分別是0, 1, ..., numCourses-1,把課程之間的依賴關(guān)系看做節(jié)點之間的有向邊。

比如說必須修完課程1才能去修課程3,那么就有一條有向邊從節(jié)點1指向3。

所以我們可以根據(jù)題目輸入的prerequisites數(shù)組生成一幅類似這樣的圖:

如果發(fā)現(xiàn)這幅有向圖中存在環(huán),那就說明課程之間存在循環(huán)依賴,肯定沒辦法全部上完;反之,如果沒有環(huán),那么肯定能上完全部課程。

好,那么想解決這個問題,首先我們要把題目的輸入轉(zhuǎn)化成一幅有向圖,然后再判斷圖中是否存在環(huán)。

如何轉(zhuǎn)換成圖呢?前文 圖論基礎 寫過圖的兩種存儲形式,鄰接矩陣和鄰接表。

以我刷題的經(jīng)驗,常見的存儲方式是使用鄰接表,比如下面這種結(jié)構(gòu):

List[]?graph;
graph[s]是一個列表,存儲著節(jié)點s所指向的節(jié)點

所以我們首先可以寫一個建圖函數(shù):

List[]?buildGraph(int?numCourses,?int[][]?prerequisites)?{
????//?圖中共有?numCourses?個節(jié)點
????List[]?graph?=?new?LinkedList[numCourses];
????for?(int?i?=?0;?i?????????graph[i]?=?new?LinkedList<>();
????}
????for?(int[]?edge?:?prerequisites)?{
????????int?from?=?edge[1];
????????int?to?=?edge[0];
????????//?修完課程?from?才能修課程?to
????????//?在圖中添加一條從?from?指向?to?的有向邊
????????graph[from].add(to);
????}
????return?graph;
}
圖建出來了,怎么判斷圖中有沒有環(huán)呢?

先不要急,我們先來思考如何遍歷這幅圖,只要會遍歷,就可以判斷圖中是否存在環(huán)了。

前文 圖論基礎 寫了 DFS 算法遍歷圖的框架,無非就是從多叉樹遍歷框架擴展出來的,加了個visited數(shù)組罷了:

//?防止重復遍歷同一個節(jié)點
boolean[]?visited;
//?從節(jié)點?s?開始?BFS?遍歷,將遍歷過的節(jié)點標記為?true
void?traverse(List[]?graph,?int?s)?{
????if?(visited[s])?{
????????return;
????}
????/*?前序遍歷代碼位置?*/
????//?將當前節(jié)點標記為已遍歷
????visited[s]?=?true;
????for?(int?t?:?graph[s])?{
????????traverse(graph,?t);
????}
????/*?后序遍歷代碼位置?*/
}
那么我們就可以直接套用這個遍歷代碼:

//?防止重復遍歷同一個節(jié)點
boolean[]?visited;

boolean?canFinish(int?numCourses,?int[][]?prerequisites)?{
????List[]?graph?=?buildGraph(numCourses,?prerequisites);

????visited?=?new?boolean[numCourses];
????for?(int?i?=?0;?i?????????traverse(graph,?i);
????}
}

void?traverse(List[]?graph,?int?s)?{
????//?代碼見上文
}
注意圖中并不是所有節(jié)點都相連,所以要用一個 for 循環(huán)將所有節(jié)點都作為起點調(diào)用一次 DFS 搜索算法。

這樣,就能遍歷這幅圖中的所有節(jié)點了,你打印一下visited數(shù)組,應該全是 true。

我曾經(jīng)說過,圖的遍歷和遍歷多叉樹差不多,所以到這里你應該都能很容易理解。

那么如何判斷這幅圖中是否存在環(huán)呢?

你可以把遞歸函數(shù)看成一個在遞歸樹上游走的指針,這里也是類似的:

你也可以把traverse看做在圖中節(jié)點上游走的指針,只需要再添加一個布爾數(shù)組onPath記錄當前traverse經(jīng)過的路徑:

boolean[]?onPath;

boolean?hasCycle?=?false;
boolean[]?visited;

void?traverse(List[]?graph,?int?s)?{
????if?(onPath[s])?{
????????//?發(fā)現(xiàn)環(huán)?。?!
????????hasCycle?=?true;
????}
????if?(visited[s])?{
????????return;
????}
????//?將節(jié)點?s?標記為已遍歷
????visited[s]?=?true;
????//?開始遍歷節(jié)點?s
????onPath[s]?=?true;
????for?(int?t?:?graph[s])?{
????????traverse(graph,?t);
????}
????//?節(jié)點?s?遍歷完成
????onPath[s]?=?false;
}
這里就有點回溯算法的味道了,在進入節(jié)點s的時候?qū)?code style="font-size: inherit;line-height: inherit;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">onPath[s]標記為 true,離開時標記回 false,如果發(fā)現(xiàn)onPath[s]已經(jīng)被標記,說明出現(xiàn)了環(huán)。

PS:參考貪吃蛇沒繞過彎兒咬到自己的場景。

這樣,就可以在遍歷圖的過程中順便判斷是否存在環(huán)了,完整代碼如下:

//?記錄一次?traverse?遞歸經(jīng)過的節(jié)點
boolean[]?onPath;
//?記錄遍歷過的節(jié)點,防止走回頭路
boolean[]?visited;
//?記錄圖中是否有環(huán)
boolean?hasCycle?=?false;

boolean?canFinish(int?numCourses,?int[][]?prerequisites)?{
????List[]?graph?=?buildGraph(numCourses,?prerequisites);

????visited?=?new?boolean[numCourses];
????onPath?=?new?boolean[numCourses];

????for?(int?i?=?0;?i?????????//?遍歷圖中的所有節(jié)點
????????traverse(graph,?i);
????}
????//?只要沒有循環(huán)依賴可以完成所有課程
????return?!hasCycle;
}

void?traverse(List[]?graph,?int?s)?{
????if?(onPath[s])?{
????????//?出現(xiàn)環(huán)
????????hasCycle?=?true;
????}

????if?(visited[s]?||?hasCycle)?{
????????//?如果已經(jīng)找到了環(huán),也不用再遍歷了
????????return;
????}
????//?前序遍歷代碼位置
????visited[s]?=?true;
????onPath[s]?=?true;
????for?(int?t?:?graph[s])?{
????????traverse(graph,?t);
????}
????//?后序遍歷代碼位置
????onPath[s]?=?false;
}


List[]?buildGraph(int?numCourses,?int[][]?prerequisites)?{
????//?代碼見前文
}
這道題就解決了,核心就是判斷一幅有向圖中是否存在環(huán)。

不過如果出題人繼續(xù)惡心你,讓你不僅要判斷是否存在環(huán),還要返回這個環(huán)具體有哪些節(jié)點,怎么辦?

你可能說,onPath里面為 true 的索引,不就是組成環(huán)的節(jié)點編號嗎?

不是的,假設下圖中綠色的節(jié)點是遞歸的路徑,它們在onPath中的值都是 true,但顯然成環(huán)的節(jié)點只是其中的一部分:

這個問題留給大家思考,我會在公眾號留言區(qū)置頂正確的答案。

那么接下來,我們來再講一個經(jīng)典的圖算法:拓撲排序。

拓撲排序

看下力扣第 210 題「課程表 II」:

這道題就是上道題的進階版,不是僅僅讓你判斷是否可以完成所有課程,而是進一步讓你返回一個合理的上課順序,保證開始修每個課程時,前置的課程都已經(jīng)修完。

函數(shù)簽名如下:

int[]?findOrder(int?numCourses,?int[][]?prerequisites);
這里我先說一下拓撲排序(Topological Sorting)這個名詞,網(wǎng)上搜出來的定義很數(shù)學,這里干脆用百度百科的一幅圖來讓你直觀地感受下:

直觀地說就是,讓你把一幅圖「拉平」,而且這個「拉平」的圖里面,所有箭頭方向都是一致的,比如上圖所有箭頭都是朝右的。

很顯然,如果一幅有向圖中存在環(huán),是無法進行拓撲排序的,因為肯定做不到所有箭頭方向一致;反過來,如果一幅圖是「有向無環(huán)圖」,那么一定可以進行拓撲排序。

但是我們這道題和拓撲排序有什么關(guān)系呢?

其實也不難看出來,如果把課程抽象成節(jié)點,課程之間的依賴關(guān)系抽象成有向邊,那么這幅圖的拓撲排序結(jié)果就是上課順序

首先,我們先判斷一下題目輸入的課程依賴是否成環(huán),成環(huán)的話是無法進行拓撲排序的,所以我們可以復用上一道題的主函數(shù):

public?int[]?findOrder(int?numCourses,?int[][]?prerequisites)?{
????if?(!canFinish(numCourses,?prerequisites))?{
????????//?不可能完成所有課程
????????return?new?int[]{};
????}
????//?...
}
PS:簡單起見,canFinish?直接復用了之前實現(xiàn)的函數(shù),但實際上可以把環(huán)檢測的邏輯和拓撲排序的邏輯結(jié)合起來,同時在 traverse 函數(shù)里完成,這個可以留給大家自己去實現(xiàn)。

那么關(guān)鍵問題來了,如何進行拓撲排序?是不是又要秀什么高大上的技巧了?

其實特別簡單,將后序遍歷的結(jié)果進行反轉(zhuǎn),就是拓撲排序的結(jié)果

直接看解法代碼:

boolean[]?visited;
//?記錄后序遍歷結(jié)果
List?postorder?=?new?ArrayList<>();

int[]?findOrder(int?numCourses,?int[][]?prerequisites)?{
????//?先保證圖中無環(huán)
????if?(!canFinish(numCourses,?prerequisites))?{
????????return?new?int[]{};
????}
????//?建圖
????List[]?graph?=?buildGraph(numCourses,?prerequisites);
????//?進行?DFS?遍歷
????visited?=?new?boolean[numCourses];
????for?(int?i?=?0;?i?????????traverse(graph,?i);
????}
????//?將后序遍歷結(jié)果反轉(zhuǎn),轉(zhuǎn)化成?int[]?類型
????Collections.reverse(postorder);
????int[]?res?=?new?int[numCourses];
????for?(int?i?=?0;?i?????????res[i]?=?postorder.get(i);
????}
????return?res;
}

void?traverse(List[]?graph,?int?s)?{
????if?(visited[s])?{
????????return;
????}

????visited[s]?=?true;
????for?(int?t?:?graph[s])?{
????????traverse(graph,?t);
????}
????//?后序遍歷位置
????postorder.add(s);
}

//?參考上一題的解法
boolean?canFinish(int?numCourses,?int[][]?prerequisites);

//?參考前文代碼
List[]?buildGraph(int?numCourses,?int[][]?prerequisites);
代碼雖然看起來多,但是邏輯應該是很清楚的,只要圖中無環(huán),那么我們就調(diào)用traverse函數(shù)對圖進行 BFS 遍歷,記錄后序遍歷結(jié)果,最后把后序遍歷結(jié)果反轉(zhuǎn),作為最終的答案。

那么為什么后序遍歷的反轉(zhuǎn)結(jié)果就是拓撲排序呢?

我這里也避免數(shù)學證明,用一個直觀地例子來解釋,我們就說二叉樹,這是我們說過很多次的二叉樹遍歷框架:

void?traverse(TreeNode?root)?{
????//?前序遍歷代碼位置
????traverse(root.left)
????//?中序遍歷代碼位置
????traverse(root.right)
????//?后序遍歷代碼位置
}
二叉樹的后序遍歷是什么時候?遍歷完左右子樹之后才會執(zhí)行后序遍歷位置的代碼。換句話說,當左右子樹的節(jié)點都被裝到結(jié)果列表里面了,根節(jié)點才會被裝進去。

后序遍歷的這一特點很重要,之所以拓撲排序的基礎是后序遍歷,是因為一個任務必須在等到所有的依賴任務都完成之后才能開始開始執(zhí)行。

你把每個任務理解成二叉樹里面的節(jié)點,這個任務所依賴的任務理解成子節(jié)點,那你是不是應該先把所有子節(jié)點處理完再處理父節(jié)點?這是不是就是后序遍歷?

下圖是一個二叉樹的后序遍歷結(jié)果:

結(jié)合這個圖說一說為什么還要把后序遍歷結(jié)果反轉(zhuǎn),才是最終的拓撲排序結(jié)果。

我們說一個節(jié)點可以理解為一個任務,這個節(jié)點的子節(jié)點理解為這個任務的依賴,但你注意我們之前說的依賴關(guān)系的表示:如果做完A才能去做B,那么就有一條從A指向B的有向邊,表示B依賴A。

那么,父節(jié)點依賴子節(jié)點,體現(xiàn)在二叉樹里面應該是這樣的:

是不是和我們正常的二叉樹指針指向反過來了?所以正常的后序遍歷結(jié)果應該進行反轉(zhuǎn),才是拓撲排序的結(jié)果。

以上,我簡單解釋了一下為什么「拓撲排序的結(jié)果就是反轉(zhuǎn)之后的后序遍歷結(jié)果」,當然,我的解釋雖然比較直觀,但并沒有嚴格的數(shù)學證明,有興趣的讀者可以自己查一下。

總之,你記住拓撲排序就是后序遍歷反轉(zhuǎn)之后的結(jié)果,且拓撲排序只能針對有向無環(huán)圖,進行拓撲排序之前要進行環(huán)檢測,這些知識點已經(jīng)足夠了。

本文就講到這里,如果我的公眾號對你有幫助,請推薦給有需要的朋友~

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

LED驅(qū)動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: 驅(qū)動電源

在工業(yè)自動化蓬勃發(fā)展的當下,工業(yè)電機作為核心動力設備,其驅(qū)動電源的性能直接關(guān)系到整個系統(tǒng)的穩(wěn)定性和可靠性。其中,反電動勢抑制與過流保護是驅(qū)動電源設計中至關(guān)重要的兩個環(huán)節(jié),集成化方案的設計成為提升電機驅(qū)動性能的關(guān)鍵。

關(guān)鍵字: 工業(yè)電機 驅(qū)動電源

LED 驅(qū)動電源作為 LED 照明系統(tǒng)的 “心臟”,其穩(wěn)定性直接決定了整個照明設備的使用壽命。然而,在實際應用中,LED 驅(qū)動電源易損壞的問題卻十分常見,不僅增加了維護成本,還影響了用戶體驗。要解決這一問題,需從設計、生...

關(guān)鍵字: 驅(qū)動電源 照明系統(tǒng) 散熱

根據(jù)LED驅(qū)動電源的公式,電感內(nèi)電流波動大小和電感值成反比,輸出紋波和輸出電容值成反比。所以加大電感值和輸出電容值可以減小紋波。

關(guān)鍵字: LED 設計 驅(qū)動電源

電動汽車(EV)作為新能源汽車的重要代表,正逐漸成為全球汽車產(chǎn)業(yè)的重要發(fā)展方向。電動汽車的核心技術(shù)之一是電機驅(qū)動控制系統(tǒng),而絕緣柵雙極型晶體管(IGBT)作為電機驅(qū)動系統(tǒng)中的關(guān)鍵元件,其性能直接影響到電動汽車的動力性能和...

關(guān)鍵字: 電動汽車 新能源 驅(qū)動電源

在現(xiàn)代城市建設中,街道及停車場照明作為基礎設施的重要組成部分,其質(zhì)量和效率直接關(guān)系到城市的公共安全、居民生活質(zhì)量和能源利用效率。隨著科技的進步,高亮度白光發(fā)光二極管(LED)因其獨特的優(yōu)勢逐漸取代傳統(tǒng)光源,成為大功率區(qū)域...

關(guān)鍵字: 發(fā)光二極管 驅(qū)動電源 LED

LED通用照明設計工程師會遇到許多挑戰(zhàn),如功率密度、功率因數(shù)校正(PFC)、空間受限和可靠性等。

關(guān)鍵字: LED 驅(qū)動電源 功率因數(shù)校正

在LED照明技術(shù)日益普及的今天,LED驅(qū)動電源的電磁干擾(EMI)問題成為了一個不可忽視的挑戰(zhàn)。電磁干擾不僅會影響LED燈具的正常工作,還可能對周圍電子設備造成不利影響,甚至引發(fā)系統(tǒng)故障。因此,采取有效的硬件措施來解決L...

關(guān)鍵字: LED照明技術(shù) 電磁干擾 驅(qū)動電源

開關(guān)電源具有效率高的特性,而且開關(guān)電源的變壓器體積比串聯(lián)穩(wěn)壓型電源的要小得多,電源電路比較整潔,整機重量也有所下降,所以,現(xiàn)在的LED驅(qū)動電源

關(guān)鍵字: LED 驅(qū)動電源 開關(guān)電源

LED驅(qū)動電源是把電源供應轉(zhuǎn)換為特定的電壓電流以驅(qū)動LED發(fā)光的電壓轉(zhuǎn)換器,通常情況下:LED驅(qū)動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: LED 隧道燈 驅(qū)動電源
關(guān)閉