陳韋傑
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
4
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# 演算法與資料結構入門 本文所有內容與資料皆由本人蒐集與撰寫,轉載請註明出處。 本篇講義尚未完成。 ![](https://hackmd.io/_uploads/Sy7FTNHea.png) # 入門介紹 - [Google Coding Style](https://google.github.io/styleguide/pyguide.html) - [甚麼是演算法?](https://jason-chen-1992.weebly.com/home/-whats-algorithm) - [甚麼是資料結構?](https://hackmd.io/@Aquamay/H1nxBOLcO/https%3A%2F%2Fhackmd.io%2F%40Aquamay%2Frk1C8ni5d) 來複習一下學習地圖,以臺大資工系必修為例: ![](https://i.imgur.com/Y2AdRO3.png =80%x) 以臺大資管系必修為例: ![](https://i.imgur.com/aC9lfrs.png =70%x) 而在演算法與資料結構的世界裡面,大概又可以分為以下幾種類別。在我們之後的課程中,會逐漸的介紹每個名詞以及其對應概念: ![image](https://hackmd.io/_uploads/BkmCHf7UR.png) 相關連結: - [How to master DSA](https://blog.algomaster.io/p/how-i-mastered-data-structures-and-algorithms) - [labuladong 的算法筆記](https://labuladong.online/algo/home/) - [演算法與資料結構](https://alrightchiu.github.io/SecondRound/mu-lu-yan-suan-fa-yu-zi-liao-jie-gou.html) # 競賽相關 適合國 / 高中生參加的相關競賽有以下: - [Bebras 國際運算思維挑戰(簡單)](https://bebras.csie.ntnu.edu.tw/) - [APCS(個人)](https://apcs.csie.ntnu.edu.tw/) - [ISSC(團體)](https://issc.csroc.org.tw/) - [TOI(資訊奧林匹亞,較難)](https://tpmso.org/toi/) 建議可以向學校老師或相關社團與補習班詢問更多細節。 ### APCS Practice - [應試相關資訊](https://apcs.csie.ntnu.edu.tw/index.php/info/) - 建議每項都要讀過,特別是環境 / 作答系統 / 考場規則 #### 實作題 - [2025/06 APCS](https://zerojudge.tw/Problems?tag=2025%E5%B9%B46%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/SJ5c1Wamxe) - [2025/01 APCS](https://zerojudge.tw/Problems?tag=2025%E5%B9%B41%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/rJlvXNP81g) - [2024/10 APCS](https://zerojudge.tw/Problems?tag=2024%E5%B9%B410%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/rJ5yJqEe1x) - [2024/06 APCS](https://zerojudge.tw/Problems?tag=2024%E5%B9%B46%E6%9C%88) and [Solution](https://hackmd.io/cn9ndy19RbKHOJKBRx4aEA) - [2024/01 APCS](https://zerojudge.tw/Problems?tag=2024%E5%B9%B41%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/SkKxG8Oua) - [2023/10 APCS](https://zerojudge.tw/Problems?tag=2023%E5%B9%B410%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/SkxXo4QGa) - [2023/06 APCS](https://zerojudge.tw/Problems?tag=2023%E5%B9%B46%E6%9C%88) and [Solution](https://hackmd.io/@bangyewu/B13lefwMp) - [All APCS Problems](https://zerojudge.tw/Problems?tag=APCS) and [Solution](https://hackmd.io/@bangyewu/B13lefwMp) and [Solution 2](https://yuihuang.com/apcs/) #### 觀念題 - [APCS 觀念題分析系列](https://hackmd.io/@howkii-studio/apcs_overview/https%3A%2F%2Fhackmd.io%2F%40howkii-studio%2Fapcs_exercise_cs) - [APCS 觀念題資源分享](https://hackmd.io/@apcser/B1N5GYEto) #### 相關資源 - 推薦:[AP325 - 從 APCS 實作題檢測三級到五級(C++)](https://drive.google.com/file/d/1hX7q3wVKkLuoMhEEm7uuLjq4BuhZAEgn/view?usp=drive_link) - [AP325(Python)](https://hackmd.io/@bangyewu/Hy2kbYLI6/%2Fg2kqHh5_Q4eQnz-mfNu3Kw) - [FB Group - APCS 實作題檢測](https://www.facebook.com/groups/359446638362710/) - [PyAp45 Playlist - Python on APCS 四五級分](https://www.youtube.com/playlist?list=PLpmg1QLbgMuRQXHRkX9iDHyAVIW1D6OJF) # 排序演算法(Sorting Algorithms) ![image](https://hackmd.io/_uploads/Hk0Xhj8x0.png) 排序演算法是一種將一組資料按照特定規則重新排列的方法。 常見的排序方法有: - 選擇排序法(Selection Sort) 每次挑最大或最小的,依序移動到對應的位置。 - 插入排序法(Insertion Sort) 照順序每次拿一個,並插入正確的位置。 - 氣泡排序法(Bubble Sort) 就像有個氣泡,兩兩比對後留下大的,每次都把最大的帶到最後面。 - 合併排序法(Merge Sort) 將陣列不斷細分,再將細分後的結果兩兩合併。 - 快速排序法(Quick Sort) 選定一個 Pivot,將比他小的丟左邊比他大的丟右邊,再針對左右兩部分進行一次相同的事情。 練習:試著實作以上五種常見的 Sorting 方法吧! > **推薦參考**:[介紹排序演算法的網站](http://notepad.yehyeh.net/Content/Algorithm/Sort/Sort.php) > **推薦參考**:[寫程式的基本功:排序演算法](https://magiclen.org/sorting-algorithm/) > 補充:[Python 中的 sorting](https://officeguide.cc/python-sort-sorted-tutorial-examples/)、[C++ 中的 sorting](https://shengyu7697.github.io/std-sort/) > 補充:[最貼近現實的排序演算法 - Timsort](https://jason18153.medium.com/%E6%9C%80%E8%B2%BC%E8%BF%91%E7%8F%BE%E5%AF%A6%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort-a75da75b65a2) 補充:還有一些很奇怪的排序方法,看了會覺得很傻眼又好笑。如果有興趣可以參考 [10 Forbidden Sorting Algorithms](https://www.youtube.com/watch?v=ktgxMtWMflU) ,舉幾個例子像是: - Bogo 排序法(Bogo Sort) 一直隨機重新排序,直到剛好符合大小順序。(猴子打字機定理) - 史達林排序法(Stalin's Sort) 遇到大小順序不對的就直接刪掉。(所以最後的 list 有可能比原本的短) # 搜尋演算法(Search Algorithms) ![image](https://hackmd.io/_uploads/HyNn3iIl0.png) 搜尋演算法是一種用來在資料集中尋找特定項目的方法或步驟。 ## 適用於未排序或已排序的資料 ### 線性搜尋(Linear Search) 直接一項一項搜尋,直到找到要的東西為止。 最基本的搜尋方式,時間複雜度為 $O(N)$。 ## 僅適用於排序的資料 以下的搜尋演算法,會利用資料**已經排序過**的性質,來加速搜尋的過程。 ### 二分搜尋(Binary Search) 很重要的搜尋演算法!將資料中間的與目標相比,若目標較大則往右再做一次搜尋,目標較小則往左,直到找到為止,可以參考 [二分搜尋法教學](https://magiclen.org/binary-search/) 與 [二分搜尋法的一些細節](https://medium.com/appworks-school/binary-search-%E9%82%A3%E4%BA%9B%E8%97%8F%E5%9C%A8%E7%B4%B0%E7%AF%80%E8%A3%A1%E7%9A%84%E9%AD%94%E9%AC%BC-%E4%B8%80-%E5%9F%BA%E7%A4%8E%E4%BB%8B%E7%B4%B9-dd2cd804aee1)。 #### Leetcode: - [35. Search Insert Position](https://leetcode.com/problems/search-insert-position/) - [69. Sqrt(x)](https://leetcode.com/problems/sqrtx/) - [278. First Bad Version](https://leetcode.com/problems/first-bad-version) ### 指數搜尋(Exponential Search) 與二分搜尋類似,只是使用 2^N 作為搜尋的 index。 ### 插補搜尋(Interpolation Search) 用線性插值的概念,來加速尋找的過程。 > **推薦參考**:[寫程式的基本功:搜尋演算法](https://magiclen.org/search-algorithm/) # 時間複雜度(Time Complexity) 時間複雜度是衡量演算法執行效率的一個重要指標,表示隨著輸入規模增加,算法執行所需時間的增長速度。 常用的符號有: - O-Notation:Big-O(Worst Case,最差) - Θ-Notation:Big-Theta(Average Case,平均) - Ω−Notation:Big-Omega(Best Case,最佳) 一般來說,我們最關心的是最差狀況下的時間複雜度(Big-O),關於詳細的數學推導可以參見補充。一些常見的時間複雜度與其例子為: * $O(1)$:陣列讀取 * $O(n)$:簡易搜尋 * $O(log n)$:二分搜尋 * $O(nlogn)$:合併排序 * $O(n^2)$:選擇排序 * $O(2^n)$:費波那契數列(遞迴版本) ![](https://hackmd.io/_uploads/rk2-JzHlp.png) 我們來看看這些例子是如何被計算出來的: - [初學者學演算法|從時間複雜度認識常見演算法](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E5%BE%9E%E6%99%82%E9%96%93%E8%A4%87%E9%9B%9C%E5%BA%A6%E8%AA%8D%E8%AD%98%E5%B8%B8%E8%A6%8B%E6%BC%94%E7%AE%97%E6%B3%95-%E4%B8%80-b46fece65ba5) - [初學者學演算法|排序法進階:合併排序法](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E6%8E%92%E5%BA%8F%E6%B3%95%E9%80%B2%E9%9A%8E-%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%E6%B3%95-6252651c6f7e) - [初學者學演算法|從費氏數列認識何謂遞迴](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E5%BE%9E%E8%B2%BB%E6%B0%8F%E6%95%B8%E5%88%97%E8%AA%8D%E8%AD%98%E4%BD%95%E8%AC%82%E9%81%9E%E8%BF%B4-dea15d2808a3) > 補充:[複雜度與漸進符號之數學](http://alrightchiu.github.io/SecondRound/complexityasymptotic-notationjian-jin-fu-hao.html) > 補充:[時間複雜度與空間複雜度](https://jason-chen-1992.weebly.com/home/time-space-complexity) ## 補充:Complexity Cheat Sheet 若不知道圖中的資料結構是什麼的話,可以先往下看。 ![](https://hackmd.io/_uploads/BkfMZStGp.png) ![](https://hackmd.io/_uploads/BJ50lHYGp.png) # 資料結構(Data Structure) 資料結構是一種設計、組織、儲存資料的方式,以實現最佳性能和效率。這些結構包含不同形式,像是陣列、鍊結列表、樹、圖等等。選擇適當的資料結構對於解決特定的問題至關重要,不同的資料結構可以用於不同的應用,並且可以極大地影響程序的運行時間和記憶體使用。 ## 陣列(Array) ![](https://hackmd.io/_uploads/SJJ_pSKMa.png) 可隨機存取的一串連續記憶體位址。 - [陣列 (Array) 簡介](https://notfalse.net/15/array-intro) > 補充:[List Are Arrays in Python](https://michaeliscoding.com/lists-are-arrays-in-python/) ### Leetcode - [26. Remove Duplicates from Sorted Array](https://leetcode.com/problems/remove-duplicates-from-sorted-array/) - [27. Remove Element](https://leetcode.com/problems/remove-element/description/) ## 鏈結串列(Linked List) ![](https://hackmd.io/_uploads/HkbKTHFG6.png) 元素間彼此串聯在一起,形成一條鍊子的資料結構。 ![image](https://hackmd.io/_uploads/SkxzAOjQp.png) 也可以是上圖雙向連結的形式。 - [Linked List:Intro(簡介)](https://alrightchiu.github.io/SecondRound/linked-list-introjian-jie.html) - [Linked List:新增資料、刪除資料、反轉](https://alrightchiu.github.io/SecondRound/linked-list-xin-zeng-zi-liao-shan-chu-zi-liao-fan-zhuan.html) ### Leetcode - [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) - [24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/) - [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) ## 堆疊(Stack, Last-In-First-Out, LIFO) ![](https://hackmd.io/_uploads/BkdcTSFMp.png) 單向進出,後進先出的資料結構,如同把東西堆疊起來。 - [Stack: Intro(簡介)](https://alrightchiu.github.io/SecondRound/stack-introjian-jie.html) - [Stack: 以Array與Linked list實作](https://alrightchiu.github.io/SecondRound/stack-yi-arrayyu-linked-listshi-zuo.html) - [Special Application: Min Stack](https://alrightchiu.github.io/SecondRound/stack-neng-gou-zai-o1qu-de-zui-xiao-zhi-de-minstack.html) ## 佇列(Queue, First-In-First-Out, FIFO) ![](https://hackmd.io/_uploads/H1tjpHFM6.png) 單向進出,先進先出的資料結構,如同在排隊一般。 - [Queue: Intro(簡介),並以Linked list實作](https://alrightchiu.github.io/SecondRound/queue-introjian-jie-bing-yi-linked-listshi-zuo.html) - [Queue: 以Array實作Queue](https://alrightchiu.github.io/SecondRound/queue-yi-arrayshi-zuo-queue.html) ## Stack & Queue in Python: Collections.deque() Deque(發音類似 Deck)為 Double-Ended Queue 的縮寫,顧名思義,Deque 是一個支援雙向存取的 Queue,也就是說,你可以從頭跟尾插入或移除元素。 - [官方說明文件](https://docs.python.org/zh-tw/3/library/collections.html#collections.deque) ```python= from collections import deque D = deque([1,2,3]) D.append(4) D.appendleft(0) print(D[2], D) print(D.pop(), D) print(D.popleft(), D) ``` ``` Output: 2 deque([0,1,2,3,4]) 4 deque([0,1,2,3]) 0 deque([1,2,3]) ``` Q:那他與 Stack 跟 Queue 有甚麼關係呢? > A: > Stack: 只使用 D.append() 與 D.pop() 的 deque > Queue: 只使用 D.append() 與 D.popleft() 的 deque > > 其實只要符合 Stack 跟 Queue 的性質都可以。 > 因此以下使用方式也對,但較不建議使用: > Stack: 只使用 D.appendleft() 與 D.popleft() 的 deque > Queue: 只使用 D.appendleft() 與 D.pop() 的 deque 以下為使用 collections.deque 來實作 stack 與 queue 的例子: ```python= from collections import deque # For Stack, you can only use the following operations S = deque() # push from a stack: S.append(val) # pop from a stack: val = S.pop() # peek from a stack: top = S[-1] # check size of a stack: size = len(S) # check stack is empty: isEmpty = (size == 0) # ========================================= # For Queue, you can only use the following operations Q = deque() # push from a queue: Q.append(val) # pop from a queue: val = Q.popleft() # peek from a queue: top = Q[0] # check size of a queue: size = len(Q) # check queue is empty: isEmpty = (size == 0) ``` 另外也可以參考 Python 官方對 [將 List 作為 Stack / Queue 使用](https://docs.python.org/zh-tw/3/tutorial/datastructures.html#using-lists-as-stacks) 的說明。 介紹完 collections.deque() 以後,讓我們來實際練習一下 Leetcode 的題目吧! ### Leetcode #### Stack - [20. Valid Parentheses](https://leetcode.com/problems/valid-parentheses/) - [155. Min Stack](https://leetcode.com/problems/min-stack/description/) - [232. Implement Queue using Stacks](https://leetcode.com/problems/implement-queue-using-stacks/) #### Queue - [225. Implement Stack using Queues](https://leetcode.com/problems/implement-stack-using-queues/) - [341. Flatten Nested List Iterator](https://leetcode.com/problems/flatten-nested-list-iterator/) - [387. First Unique Character in a String](https://leetcode.com/problems/first-unique-character-in-a-string/) ## 樹(Tree) ![](https://hackmd.io/_uploads/S13npBFMp.png) 基本單位為 Node,且只有一個 Node 是 Root(無人指向的 Node),且不存在任何 Cycle。每個 Node 可以指向多個 Child。 - [Tree(樹): Intro(簡介)](https://alrightchiu.github.io/SecondRound/treeshu-introjian-jie.html) ### 二元樹(Binary Tree) ![image](https://hackmd.io/_uploads/SJQuAds7a.png) 當樹中的每個 Node 都只有兩個 Child,即為 Binary Tree。 - [Binary Tree: Intro(簡介)](https://alrightchiu.github.io/SecondRound/binary-tree-introjian-jie.html) - [Binary Tree: Traversal(尋訪)](https://alrightchiu.github.io/SecondRound/binary-tree-traversalxun-fang.html) 以上面的 Binary Tree 為例: - Pre-Order Traversal: ABDECFG - In-Order Traversal: DBEAFCG - Post-Order Traversal: DEBFGCA ![image](https://hackmd.io/_uploads/rJEw81ajp.png) > 延伸閱讀:[Binary Tree: 建立一棵Binary Tree](https://alrightchiu.github.io/SecondRound/binary-tree-jian-li-yi-ke-binary-tree.html) ### 二元搜尋樹(Binary Search Tree) ![image](https://hackmd.io/_uploads/rJ5d1tom6.png) 若一個二元樹中,所有 Node 都滿足 Node.left.value < Node.value < Node.right.value,就稱為二元搜尋樹。二元搜尋樹有良好的排序特質,可以幫助我們找到想要的資料。 - [Binary Search Tree: Intro(簡介)](https://alrightchiu.github.io/SecondRound/binary-search-tree-introjian-jie.html) - [Binary Search Tree: Search(搜尋資料)、Insert(新增資料)](https://alrightchiu.github.io/SecondRound/binary-search-tree-searchsou-xun-zi-liao-insertxin-zeng-zi-liao.html) - [Binary Search Tree: Sort(排序)、Delete(刪除資料)](https://alrightchiu.github.io/SecondRound/binary-search-tree-sortpai-xu-deleteshan-chu-zi-liao.html) > 注意:在 Binary Search Tree 上做 In-Order Traversal,即可得到排序好的資料! ### 進階 - 紅黑樹(Red Black Tree) 紅黑樹是一種進階的 BST(Binary Search Tree),透過將每個節點塗上紅色或黑色,並在新增或刪除資料時進行適當的結構調整(旋轉子樹),可以維持 BST 的高度不會相差太多,進而在搜尋時能有較好的效率。細節太過複雜這邊暫時跳過,之後有時間 or 有興趣再回來講。 ![image](https://hackmd.io/_uploads/HyclMKj7p.png) - [Red Black Tree: Intro(簡介)](https://alrightchiu.github.io/SecondRound/red-black-tree-introjian-jie.html) > 延伸閱讀: > - [Red Black Tree: Rotation(旋轉)](https://alrightchiu.github.io/SecondRound/red-black-tree-rotationxuan-zhuan.html) > - [Red Black Tree: Insert(新增資料)與Fixup(修正)](https://alrightchiu.github.io/SecondRound/red-black-tree-insertxin-zeng-zi-liao-yu-fixupxiu-zheng.html) > - [Red Black Tree: Delete(刪除資料)與Fixup(修正)](https://alrightchiu.github.io/SecondRound/red-black-tree-deleteshan-chu-zi-liao-yu-fixupxiu-zheng.html) ### 進階 - AVL 樹(AVL Tree) ![image](https://hackmd.io/_uploads/r1GAdTXt6.png) AVL Tree 是一種 Binary search tree 實做方式,大部分的實做方式與 BST 一樣,差異在於 AVL tree 在過程中會透過計算並調整樹的結構來讓樹維持平衡,而不會導致 BST 過度傾斜(不平衡),與紅黑樹的目標類似。細節太過複雜這邊暫時跳過,之後有時間 or 有興趣再回來講。 > 延伸閱讀:[資料結構與演算法:AVL Tree](https://josephjsf2.github.io/data/structure/and/algorithm/2019/06/22/avl-tree.html) ### Leetcode #### Basic - Traversal & Search Please try to use both recursion and stack/queue to solve problem 102 and 144. (Hint: For 102(Level-Order) you should use **Queue**, for 144(Pre-Order) you should use **Stack**) - [94. Binary Tree Inorder Traversal](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) - [102. Binary Tree Level Order Traversal](https://leetcode.com/problems/binary-tree-level-order-traversal/description/) - [144. Binary Tree Preorder Traversal](https://leetcode.com/problems/binary-tree-preorder-traversal/description/) - [145. Binary Tree Postorder Traversal](https://leetcode.com/problems/binary-tree-postorder-traversal/description/) - [700. Search in A Binary Search Tree](https://leetcode.com/problems/search-in-a-binary-search-tree/description/) #### Applications - [100. Same Tree](https://leetcode.com/problems/same-tree/description/) - [226. Invert Binary Tree](https://leetcode.com/problems/invert-binary-tree/description/) - [530. Minimum Absolute Difference in BST](https://leetcode.com/problems/minimum-absolute-difference-in-bst/description/) - [1026. Maximum Difference Between Node and Ancestor](https://leetcode.com/problems/maximum-difference-between-node-and-ancestor/description/) - [1609. Even Odd Tree](https://leetcode.com/problems/even-odd-tree/description) #### Tools - [BT Visualizer](https://eniac00.github.io/btv/) ## 圖(Graph) ![](https://hackmd.io/_uploads/S1v-AHFM6.png) 圖比樹的限制更少,給定一群節點與相連的邊,即可稱為圖。邊可以分為有向與無向,節點之間的連結並沒有限制,也因此圖並不像樹有 root、parent、siblings、height 等等屬性,且有可能會有 cycle 的出現。 ### Graph & Tree 的比較 ![](https://hackmd.io/_uploads/HkvhyNtf6.png) Tree 可以被看成一種特殊的 Graph,就像 Binary Tree 是一種特殊的 Tree 一樣。 ### Graph 的表示法 ![image](https://hackmd.io/_uploads/H1otPi8gA.png) - [Graph: Intro(簡介)](https://alrightchiu.github.io/SecondRound/graph-introjian-jie.html) ### Graph 的搜尋 ![image](https://hackmd.io/_uploads/r1Ef-7Pnp.png) 最常見、也最常使用的搜尋方法有兩種:BFS 與 DFS。兩者的差異在於搜尋的優先順序不同,並且分別可以使用 Queue 與 Stack 來實作。 #### 廣度優先搜尋(Breadth First Search, BFS) - [Breadth First Search or BFS for a Graph](https://www.geeksforgeeks.org/breadth-first-search-or-bfs-for-a-graph/) #### 深度優先搜尋(Depth First Search, DFS) - [Depth First Search or DFS for a Graph](https://www.geeksforgeeks.org/depth-first-search-or-dfs-for-a-graph/) #### 那 DFS 與 BFS 可以應用在 Tree 上嗎? 因為 Tree 也是 Graph 的一種,所以當然可以,而且非常常用! > 延伸閱讀:[DFS、BFS 與 Pre-Order、In-Order、Post-Order、Level-Order 的關係](https://stackoverflow.com/a/57598470/15894431) ### Leetcode - [133. Clone Graph](https://leetcode.com/problems/clone-graph/description/) - [797. All Paths From Source to Target](https://leetcode.com/problems/all-paths-from-source-to-target/description/) - [841. Keys and Rooms](https://leetcode.com/problems/keys-and-rooms/) - [997. Find the Town Judge](https://leetcode.com/problems/find-the-town-judge/description/) --- #### 在圖中找環(Cycle) 上面的兩個例子中,都使用 `visited` 來紀錄該節點是否被拜訪過。因此,在**無向圖**中,若我們下一個要拜訪的節點的 `visited` 為 `True`,那我們就可以知道該圖中有環的存在。那在**有向圖**中呢?以下圖來說: ![image](https://hackmd.io/_uploads/rkSj6pq0a.png) 若我們在右邊的有向圖 b 中,用相同的 DFS 來尋找是否有環,假設第一次尋訪的路徑為 (1,2,4,5),而第二次尋訪退回到 2 並往下尋訪 (3,4),我們這時候會發現 `visited[4] == True`,並且判定該圖有環,但實際上是沒有的。那我們該怎麼做呢? 在往下看之前,先自己想想看喔! --- 答案:使用三個不同的值來表示每個節點不同的尋訪狀態(黑、白、灰)。黑色代表已經完成搜尋,白色代表還沒搜尋過,灰色代表正在這條 path 上搜尋,等搜尋完成後就會改為黑色。因此,若我們搜尋時遇到灰色節點,就可以知道該圖存在 cycle! ```python= # Determine if a directed graph is acyclic # True: Acyclic; False: Cyclic graph = ... # adjacency list representation N = len(graph) visited = [0] * N def checkNoCycle(node): if visited[node] == -1: # gray, currently visiting return False elif visited[node] == 1: # black, done visiting return True else: # white, not visited yet (visited[node] == 0) visited[node] = -1 for neighbor in graph[node]: if not dfs(neighbor): return False visited[node] = 1 # print(node) return True ``` > 補充:[The purpose of grey node in graph depth-first search](https://cs.stackexchange.com/questions/9676/the-purpose-of-grey-node-in-graph-depth-first-search) ### 連通分量(Connected Components) ![image](https://hackmd.io/_uploads/r1EGVkc-A.png) ![ConnectedComponent4](https://hackmd.io/_uploads/BkStEJ9WR.png) - [Connected Component - 演算法筆記](https://web.ntnu.edu.tw/~algo/ConnectedComponent.html) - [Strongly Connected Components - Kosaraju 演算法](https://www.cnblogs.com/RioTian/p/14026585.html) > 補充:[Connected Components in an Undirected Graph](https://www.geeksforgeeks.org/connected-components-in-an-undirected-graph/) > 補充:[Strongly Connected Components](https://www.geeksforgeeks.org/strongly-connected-components/) > 補充:[Tarjan 演算法與 Kosaraju 演算法](https://hackmd.io/@erichung0906/HkjZBH_IK) ### Leetcode - [207. Course Schedule](https://leetcode.com/problems/course-schedule/description/) - [1971. Find if Path Exists in Graph](https://leetcode.com/problems/find-if-path-exists-in-graph/description/) > 延伸閱讀: > - [Graph: Breadth-First Search(BFS,廣度優先搜尋)](https://alrightchiu.github.io/SecondRound/graph-breadth-first-searchbfsguang-du-you-xian-sou-xun.html) > - [Graph: Depth-First Search(DFS,深度優先搜尋)](https://alrightchiu.github.io/SecondRound/graph-depth-first-searchdfsshen-du-you-xian-sou-xun.html) > - [Graph: 利用DFS和BFS尋找Connected Component](https://alrightchiu.github.io/SecondRound/graph-li-yong-dfshe-bfsxun-zhao-connected-component.html) > - [Graph: 利用DFS尋找Strongly Connected Component(SCC)](https://alrightchiu.github.io/SecondRound/graph-li-yong-dfsxun-zhao-strongly-connected-componentscc.html) > - [Graph: 利用DFS尋找DAG的Topological Sort(拓撲排序)](https://alrightchiu.github.io/SecondRound/graph-li-yong-dfsxun-zhao-dagde-topological-sorttuo-pu-pai-xu.html) ## 矩陣(Matrix) ![image](https://hackmd.io/_uploads/SyxulaZA6.png) 在介紹 Graph 的表示方法時,我們提到了兩種不同的表示方式,分別是鄰接串列(Adjacency List)與鄰接矩陣(Adjacency Matrix)。不過其實,矩陣本身也可以被當作一種資料結構來使用,舉凡像是迷宮、地圖等等具有 2D 性質的資料型態,都可以使用矩陣來表示。 Matrix 的相關操作其實都與 Graph 差不多,不外乎就是在 Matrix 中進行搜尋或遍歷,但因為我們可以以 $O(1)$ 的複雜度,直接取用 Matrix 內部中的任一元素(`matrix[row][col]`),所以 Matrix 在某些應用上具有較為快速的優勢。那接著就讓我們直接實戰演練吧! ### Leetcode - [200. Number of Islands](https://leetcode.com/problems/number-of-islands/description/) - [994. Rotting Oranges](https://leetcode.com/problems/rotting-oranges/description/) - [1351. Count Negative Numbers in a Sorted Matrix](https://leetcode.com/problems/count-negative-numbers-in-a-sorted-matrix/description/) ## 堆積(Heap) ![](https://hackmd.io/_uploads/BJqGCStzT.png) 一種具有特殊性質(Parent Node 大於或小於 Child Node)的 Complete Binary Tree。 ### 堆積排序法(Heap Sort) - [[演算法(Algorithm)] 堆積排序法(Heap Sort)](https://notepad.yehyeh.net/Content/Algorithm/Sort/Heap/Heap.php) 對陣列做 Heapify 變成 Max Heap,再不斷把最大值往後擺。 > 練習:試著實作 Heap Sort 吧! ## 優先佇列(Priority Queue) ![image](https://hackmd.io/_uploads/S1CWOylZ0.png) 具有優先權(Priority)來代表其進出順序的 Queue。 - [Priority Queue:Intro(簡介)](https://alrightchiu.github.io/SecondRound/priority-queueintrojian-jie.html) - [Priority Queue:Binary Heap](https://alrightchiu.github.io/SecondRound/priority-queuebinary-heap.html) ### Stack / Queue v.s. Priority Queue? Stack 與 Queue 其實可以被視作特殊狀況的 Priority Queue: - Stack:加入 Priority Queue 的 Priority 是嚴格遞增的 - 因此最後加入的元素 Priority 最高,會先被丟出 Stack - Queue:加入 Priority Queue 的 Priority 是嚴格遞減的 - 因此最先加入的元素 Priority 最高,會先被丟出 Queue <!-- In a stack, the priority of each inserted element is monotonically increasing; thus, the last element inserted is always the first retrieved. In a queue, the priority of each inserted element is monotonically decreasing; thus, the first element inserted is always the first retrieved. --> ### Priority Queue v.s. Heap? - Priority Queue:一種**抽象資料類別**,著重的點是描述這個資料類別應該具有甚麼性質與操作方法,不直接討論實作方法。 - Heap:一種**資料結構**,著重的點在以特定的結構儲存資料。 因為他們具有類似的性質,所以用 Heap 的結構來實作 Priority Queue 這個資料類別,可以說是非常適合,也因此乍看之下他們好像是一樣的東西。但兩者概念上有些微不同,亦可以用其他方式來實做 Priority Queue。 > 補充:[Difference between priority queue and a heap](https://stackoverflow.com/a/18993313/15894431) > 補充:[What's the difference between heapq and PriorityQueue in python?](https://stackoverflow.com/questions/36991716/whats-the-difference-between-heapq-and-priorityqueue-in-python) ## Heap / Priority Queue in Python: heapq - [Python heapq](https://docs.python.org/zh-tw/3.10/library/heapq.html) ```python= import heapq heap = [9,7,5,3,1] heapq.heapify(heap) heapq.heappush(heap, 2) _ = heapq.heappop(heap) _ = heapq.heappushpop(heap, 2) # Will not modify the heap (but inefficient) klargest = heapq.nlargest(k, heap) ksmallest = heapq.nsmallest(k, heap) # Use heap as priority queue nodes = [(5, 'A'), (2, 'B'), (9, 'C')] heap = [] for node in nodes: heapq.heappush(heap, (node[0], node[1])) # (priority, value) ``` ### Leetcode - [703. Kth Largest Element in a Stream](https://leetcode.com/problems/kth-largest-element-in-a-stream/description/) - [1046. Last Stone Weight](https://leetcode.com/problems/last-stone-weight/description/) - [1642. Furthest Building You Can Reach](https://leetcode.com/problems/furthest-building-you-can-reach/description/) ## 雜湊表(Hash Table) ![](https://hackmd.io/_uploads/r1cQCHKMa.png) ### 雜湊函數的性質 雜湊函式是一種把輸入數據轉換成固定長度的字符串(雜湊值)的工具。它有以下特點: * 固定長度輸出:無論輸入數據有多長,輸出都是固定長度。 * 無法回推:從雜湊值無法反推出原始輸入(即單向性),保證了數據的不可逆性。 * 抗碰撞性:難以找到兩個不同的輸入有相同的雜湊值。 * 快速計算:能快速生成雜湊值。 ![image](https://hackmd.io/_uploads/ByQ3G5S7R.png) ![image](https://hackmd.io/_uploads/S1vaz9S7A.png) ![image](https://hackmd.io/_uploads/ByaTfqrXA.png) - [[資料結構] 雜湊 (Hash)](https://ithelp.ithome.com.tw/articles/10208884) > Images are from [什麼是Hash Function? 有什麼特性及用途?](https://homuchen.com/posts/what-is-hash-function-its-properties-and-usages/) ### 雜湊函數的應用 * 資料完整性驗證:確保收到的資料是完整的,或確保文件未被篡改。 * 密碼儲存:儲存密碼的雜湊值,而不是明文密碼,提高安全性。常見的加密方式如 MD5、SHA-256 等等,若有興趣可以再研究密碼學。 * 資料結構:用於實現雜湊表,能夠快速存取資料。使用雜湊函數的常見資料結構包含集合(Set)、字典(Dictionary)等等。 * 數位簽章:保護資料完整性和驗證身份。 > 補充:[雜湊函數 - 維基百科](https://zh.wikipedia.org/zh-tw/%E6%95%A3%E5%88%97%E5%87%BD%E6%95%B8) ### Leetcode - [49. Group Anagrams](https://leetcode.com/problems/group-anagrams/description/) - [387. First Unique Character in a String](https://leetcode.com/problems/first-unique-character-in-a-string/description/) - [2441. Largest Positive Integer That Exists With Its Negative](https://leetcode.com/problems/largest-positive-integer-that-exists-with-its-negative/description/) <!-- - [242. Valid Anagram](https://leetcode.com/problems/valid-anagram/description/) --> ## 字典樹(Prefix Tree / Trie) ![image](https://hackmd.io/_uploads/BJdBwzXIR.png) 以 N-ary Tree 的方式來表示一個字典,好處是當彼此有共同前綴(Common Prefix)時可以增加儲存與查找效率,常用於搜尋時的自動補字。 - [Trie(字典樹)](https://hackmd.io/@TienYi/trie) 實作上的話就跟正常的 N-ary Tree 差不多,可以使用 Dictionary 這種 key-value 的 Pair 來協助,同時要記得加註每個字結束的位置。以下為搭配`collections.defaultdict` 的 Trie 實作: ```python= class Trie: def __init__(self): nested_ddict = lambda: defaultdict(nested_ddict) self.tree = nested_ddict() def insert(self, word: str) -> None: curr_tree = self.tree for c in word: curr_tree = curr_tree[c] curr_tree['END'] = '' def _search_tool(self, dest: str) -> tuple[bool, defaultdict]: curr_tree = self.tree for c in dest: if c not in curr_tree: return False, defaultdict() curr_tree = curr_tree[c] return True, curr_tree def search(self, word: str) -> bool: status, curr_tree = self._search_tool(word) if not status: return False return True if 'END' in curr_tree else False def startsWith(self, prefix: str) -> bool: status, curr_tree = self._search_tool(prefix) return status ``` ### Leetcode <!-- - [14. Longest Common Prefix](https://leetcode.com/problems/longest-common-prefix/description/) --> - [208. Implement Trie (Prefix Tree)](https://leetcode.com/problems/implement-trie-prefix-tree/description/) <!-- - [676. Implement Magic Dictionary](https://leetcode.com/problems/implement-magic-dictionary/description/) --> ## 並查集(Disjoint Set / Union Find) ![image](https://hackmd.io/_uploads/ry305yeWA.png) 由多個彼此沒有交集的 Set 組成,常用在需要分組與合併的情境中。 - [Union-Find / Disjoint-Set – 陪你刷題](https://haogroot.com/2021/01/29/union_find-leetcode/) 一個包含路徑壓縮,並以 rank (size of set) 來作為 union 依據的 Union-Find 資料結構,大概會是如下的形式: ```python= class UF: def __init__(self, N): self.parent = list(range(N)) self.size = [1] * N self.count = N def union(self, x, y): rootX = self.find(x) rootY = self.find(y) if rootX != rootY: self.count -= 1 if self.size[rootX] < self.size[rootY]: self.parent[rootX] = rootY self.size[rootY] += self.size[rootX] else: self.parent[rootY] = rootX self.size[rootX] += self.size[rootY] def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x ``` ### Leetcode - [200. Number of Islands](https://leetcode.com/problems/number-of-islands/description/) - [547. Number of Provinces](https://leetcode.com/problems/number-of-provinces/description/) - [1971. Find if Path Exists in Graph](https://leetcode.com/problems/find-if-path-exists-in-graph/description/) # 其他演算法與常見解題技巧 演算法(Algorithm)是在有限的步驟之內,提供明確的法則,求出問題正確答案的程序。它可以是一種方法、法則或者程序,讓資料可按照預先設計的方式處理。 - [認識演算法](https://hackmd.io/@howkii-studio/Bkf-2DQiw/https%3A%2F%2Fhackmd.io%2F%40howkii-studio%2Falgorithm) - 推薦補充閱讀:[演算法筆記](https://web.ntnu.edu.tw/~algo/) - [Algorithm Design](https://web.ntnu.edu.tw/~algo/AlgorithmDesign.html) ## 暴力法(Brute Force) 暴力法(Brute Force)是一種基本的解題策略,透過系統地嘗試所有可能的解決方案來找出答案。它的原理很簡單:列舉所有可能性,不斷嘗試,直到找到符合要求的解。 這種方法的優點是實現簡單,且保證能找到存在的解。但它的效率通常較低,特別是在處理大規模問題時,因此暴力法常用於簡單問題或作為其他算法的比較基準。 以下補充幾個以前教過,但其實都跟暴力法有關的演算法。 > 補充:[【舌尖上的演算法】Day6 -- Brute Force - Selection Sort](https://jumperc2p.github.io/InformisTry/posts/ithome-triathlon/brute_force_selection_sort/) > 補充:[【舌尖上的演算法】Day7 -- Brute Force - Bubble Sort](https://jumperc2p.github.io/InformisTry/posts/ithome-triathlon/brute_force_bubble_sort/) > 補充:[【舌尖上的演算法】Day8 -- Brute Force - Knapsack](https://jumperc2p.github.io/InformisTry/posts/ithome-triathlon/brute_force_knapsack/) > 補充:[【舌尖上的演算法】Day9 -- Brute Force - DFS & BFS](https://jumperc2p.github.io/InformisTry/posts/ithome-triathlon/brute_force_dfs_bfs/) ## 貪婪演算法(Greedy Method) ![image](https://hackmd.io/_uploads/SkFiTx6U0.png) 貪婪演算法(Greedy Method)是一種在每一步都做出當前最佳選擇的問題解決策略。它通過在每個決策點選擇局部最優解,希望最終能達到整體最優解。 貪婪演算法的主要特點: 1. 在每一步選擇當前看起來最好的選項 2. 一旦做出選擇,就不會回頭重新考慮 3. 期望這些局部最優選擇能導致整體最優解 這種演算法通常簡單高效,適用於某些特定問題,然而它並不總是能得到最佳解決方案。貪婪演算法的優勢在於易於實現和速度快,但可能會錯過需要前瞻性思考或回溯的更優解。 ### Leetcode - [455. Assign Cookies](https://leetcode.com/problems/assign-cookies/description/) - [948. Bag of Tokens](https://leetcode.com/problems/bag-of-tokens/description/) <!-- - [2037. Minimum Number of Moves to Seat Everyone](https://leetcode.com/problems/minimum-number-of-moves-to-seat-everyone/description/) --> ## 回溯法(Backtracking) ![image](https://hackmd.io/_uploads/Bkad6xpLC.png) 回溯法(Backtracking)也是暴力法的一種,特別適用於需要窮舉所有可能性的情況。 回溯法的基本思想是: 1. 從一個可能的起點開始。 2. 按照問題要求,逐步向前探索。 3. 如果發現當前路徑無法得到有效解,就退回到上一步。 4. 在上一步選擇一個不同的方向繼續探索。 5. 重複這個過程,直到找到解或者探索完所有可能性。 回溯法的優點是能夠系統地搜索所有可能性,缺點是在最壞情況下可能需要很長的運行時間。這種方法像是在迷宮中探路:如果碰到死路,就退回到上一個路口,選擇另一條路繼續前進。 - [一次看懂遞迴 (Recursion) 的思維模式(三)- 窮舉可能性(Backtracking)](https://medium.com/appworks-school/%E9%80%B2%E5%85%A5%E9%81%9E%E8%BF%B4-recursion-%E7%9A%84%E4%B8%96%E7%95%8C-%E4%B8%89-d2fd70b5b171) ### Leetcode - [17. Letter Combinations of a Phone Number](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/) - [46. Permutations](https://leetcode.com/problems/permutations/description/) ## 分治法(Divide-and-Conquer) ![image](https://hackmd.io/_uploads/r1NtDxTUR.png) Divide-and-Conquer又稱為分治法。其中 Divide 指的是將一個較大的問題不斷切割成小問題。而 Conquer 是當最後切割成的小問題簡單到可以直接解決,就可以組合成大問題的答案。在程式語言中時常都會用到分治法的觀念,並結合遞迴的概念求解。 > Source: [分而治之法與遞迴關係](https://hackmd.io/@rd2865OAQZSLjri24DYcow/HkmNAB4aO) 還記得之前教過的 Merge Sort 嗎?(合併排序法:將陣列不斷細分,再將細分後的結果兩兩合併。)其實 Merge Sort 的精神本質上就是 Divide-and-Conquer! - [【舌尖上的演算法】Day15 -- Divide and Conquer - Merge Sort](https://jumperc2p.github.io/InformisTry/posts/ithome-triathlon/dicon-ms/) ### Leetcode <!-- - [105. Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) - [106. Construct Binary Tree from Inorder and Postorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/) --> - [108. Convert Sorted Array to Binary Search Tree](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) - [148. Sort List](https://leetcode.com/problems/sort-list/description/) ## 動態規劃(Dynamic Programming) ![image](https://hackmd.io/_uploads/SkqV4HbSR.png) **Dynamic Programming = Divide-and-Conquer + Memoization** 動態規劃是一種解決問題的方法,它將大問題拆解成小問題(Divide-and-Conquer),並記錄每個小問題的解答(Memoization),以便後續使用。這樣做能夠節省計算時間,讓我們更有效地解決問題,針對某些「透過前面答案來計算後面答案的問題」特別有用。 - 推薦閱讀:[演算法筆記:DP](https://web.ntnu.edu.tw/~algo/DynamicProgramming.html) ### Recursion v.s. Dynamic Programming? 我們來看看這題(Leetcode [509. Fibonacci Number](https://leetcode.com/problems/fibonacci-number/description/)): - 費波納契數列 給定輸入整數n,輸出第 n 項費波納契數列 舉例:n = 8,輸出為 21 (1 1 2 3 5 8 13 21) Recursion 寫法: ```python= # Recursion def fibonacci_recursive(n): if n <= 0: return 0 elif n == 1: return 1 else: return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2) ``` Dynamic Programming 寫法: ```python= # Dynamic Programming def fibonacci_dp(n): if n <= 0: return 0 elif n == 1: return 1 else: fib = [0, 1] + [-1] * (n - 1) for i in range(2, n + 1): fib[i] = fib[i - 1] + fib[i - 2] return fib[n] ``` #### 時間複雜度分析 接著我們來測量一下他們的速度差異: ```python= import timeit print("遞迴方法耗時: %.6f 秒"%(timeit.timeit(lambda: fibonacci_recursive(30),number=1))) print("動態規劃方法耗時: %.6f 秒"%(timeit.timeit(lambda: fibonacci_dp(30),number=1))) ``` ``` Output: 遞迴方法耗時: 1.373473 秒 動態規劃方法耗時: 0.000012 秒 ``` 為什麼會差這麼多呢?我們來分析看看兩種方法的時間複雜度: - 遞迴版本 時間複雜度:$O(2^n)$。對於每個 fibonacci_recursive(n),它會呼叫 fibonacci_recursive(n-1) 和 fibonacci_recursive(n-2),這種重複計算會導致呼叫函數的次數呈指數級數增長,因此時間複雜度為 $O(2^n)$。 - 動態規劃版本 時間複雜度:$O(n)$。它只需要計算一次每個費氏數列的值,並將結果存入列表中。迴圈從 2 執行到 n,所以時間複雜度為 $O(n)$。 對於較小的 n,遞迴可能更簡單易懂,並且兩者的速度可能相差不大。但是當 n 較大時,遞迴所需的時間會嚴重惡化,相較之下動態規劃會有更好的效能表現。 看到上述兩者的時間複雜度差異之大,是不是發現動態規劃的強大了? ### Leetcode <!-- - [62. Unique Paths](https://leetcode.com/problems/unique-paths/description/) --> - [63. Unique Paths II]((https://leetcode.com/problems/unique-paths-ii/description/)) - [64. Minimum Path Sum](https://leetcode.com/problems/minimum-path-sum/description/) - [70. Climbing Stairs](https://leetcode.com/problems/climbing-stairs/description/) - [198. House Robber](https://leetcode.com/problems/house-robber/description/) <!-- ### 動態規劃經典問題:背包問題(Knapsack Problem) --> > 延伸閱讀:[演算法筆記:Knapsack Problem](https://web.ntnu.edu.tw/~algo/KnapsackProblem.html) ## 前綴和(Prefix Sum) ![image](https://hackmd.io/_uploads/HJHhReT8R.png) 前綴和是一種用來快速計算數列中某一段連續元素的總和的技巧。它的概念是先建立一個新的數列,其中每個元素代表原數列從開頭到該位置的和。這樣一來,當我們需要計算某段範圍的總和時,只要用前綴和數列中對應位置的值相減,就能快速得出答案。 舉例來說,假設有一個數列 `A=[2, 3, 5, 7]`,我們可以建立前綴和數列 `P=[0, 2, 5, 10, 17]`。如果想知道從第2到第4個元素的總和(即 `A[2] + A[3] + A[4]`),只要計算 `P[4] − P[1]`,就可以得到總和 15。建立數列的過程中可以搭配動態規劃的技巧一起使用。 - [【演算法】前綴和(Prefix Sum)](https://huangmayor0905.github.io/cs/algo/prefix-sum/) ### Leetcode - [724. Find Pivot Index](https://leetcode.com/problems/find-pivot-index/description/) - [1588. Sum of All Odd Length Subarrays](https://leetcode.com/problems/sum-of-all-odd-length-subarrays/description/) > 延伸閱讀:[Python-LeetCode 581 第四招 前綴和 Prefix Sum](https://hackmd.io/@bangyewu/rJRpH9dhh) > 延伸閱讀:[線段樹](https://hackmd.io/@wiwiho/CPN-segment-tree) ## 字串比對(String Matching) ### KMP 演算法 ![image](https://hackmd.io/_uploads/ry2SjfO4C.png) KMP 演算法是一種用於字串搜尋的高效方法。透過建立部分配對表(最長的相同前後綴),可以快速定位搜尋字串中的可能配對位置,減少不必要的重複比較,從而加速搜尋過程。 KMP 演算法其實相對複雜且較難實作,我個人認為有大概理解概念就好,實際上也很少遇到需要直接實作該演算法的情境。 - [初學者學 KMP 演算法](https://yeefun.github.io/kmp-algorithm-for-beginners/) > 補充:[KMP algorithm,從自學到放棄 (1)](https://medium.com/@c.s.fangyolk/kmp-%E6%BC%94%E7%AE%97%E6%B3%95-%E5%BE%9E%E8%87%AA%E5%AD%B8%E5%88%B0%E6%94%BE%E6%A3%84-1-7f71e65839a0) > 補充:[KMP algorithm,從自學到放棄 (2)](https://medium.com/@c.s.fangyolk/kmp-%E6%BC%94%E7%AE%97%E6%B3%95-%E5%BE%9E%E8%87%AA%E5%AD%B8%E5%88%B0%E6%94%BE%E6%A3%84-2-94dda22f80b2) > 補充:[演算法筆記:substring](https://web.ntnu.edu.tw/~algo/Substring.html) > 補充:[KMP Visualization](https://cmps-people.ok.ubc.ca/ylucet/DS/KnuthMorrisPratt.html) ### Leetcode - [28. Find the Index of the First Occurrence in a String](https://leetcode.com/problems/find-the-index-of-the-first-occurrence-in-a-string/description/) ## 位元運算(Bit Manipulation) ![image](https://hackmd.io/_uploads/HyHM0lpLR.png) Bit manipulation 是在電腦中用「位元」(bit)來進行操作的技巧,這種技巧常在程式設計競賽或像 Leetcode 這樣的題目中出現。位元是電腦中最基本的單位,由 0 或 1 組成,這跟開關一樣,只有開或關的狀態。這些 0 與 1 會再經由[二進位制](https://zh.wikipedia.org/wiki/%E4%BA%8C%E8%BF%9B%E5%88%B6)與[二補數](https://zh.wikipedia.org/wiki/%E4%BA%8C%E8%A3%9C%E6%95%B8)的方式,組成所有的其他數字。常見的位元操作有:AND, OR, XOR, NOT, Shift 等等,參考以下: ![image](https://hackmd.io/_uploads/HkgbJHIDR.png) - [Python 的位元運算](https://hackmd.io/@apcser/H14FONT4n?utm_source=preview-mode&utm_medium=rec) ### Leetcode - [231. Power of Two](https://leetcode.com/problems/power-of-two/description/) - [268. Missing Number](https://leetcode.com/problems/missing-number/description/) - [2220. Minimum Bit Flips to Convert Number](https://leetcode.com/problems/minimum-bit-flips-to-convert-number/description/) ## 雙重指標(Two Pointers) ![image](https://hackmd.io/_uploads/BkqW-uvkyl.png) 常用於陣列當中,用兩個指標指著不同的位置,與滑動視窗(Sliding Window)有異曲同工之妙,兩者基本上是相同技巧。 ### 快慢指標(Fast & Slow Pointers) 此種技巧為雙重指標的特殊版本,使用一快一慢的指標(一次走不同步數 or 一個先走一個後走),常用在 Linked List 當中。 這樣可以幹嘛呢?我們來看看以下的例子: #### 尋找中點 ![image](https://hackmd.io/_uploads/H1c7bdv1Jl.png) #### 判斷 Cycle 是否存在 Linked List 中 ![image](https://hackmd.io/_uploads/HkLZzOPJJg.png) ![image](https://hackmd.io/_uploads/Byf5Wdv11e.png) #### 尋找倒數第 K 個節點 ![image](https://hackmd.io/_uploads/ry3NMVU8yx.png) - [演算法筆記系列 — Two Pointer 與Sliding Window](https://medium.com/%E6%8A%80%E8%A1%93%E7%AD%86%E8%A8%98/%E6%BC%94%E7%AE%97%E6%B3%95%E7%AD%86%E8%A8%98%E7%B3%BB%E5%88%97-two-pointer-%E8%88%87sliding-window-8742f45f3f55) ### Leetcode - [75. Sort Colors](https://leetcode.com/problems/sort-colors/) - [141. Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/) - [876. Middle of the Linked List](https://leetcode.com/problems/middle-of-the-linked-list/) ## 圖的進階議題(Advanced Topic on Graph) ### 最短路徑(Shortest Path) 最短路徑問題(Shortest Path Problem)是圖論中的一個重要課題,目的是尋找從一個起始節點到其他節點的最短路徑。相關的應用像是地圖中的導航系統、網路路由選擇等等,背後都是使用最短路徑的演算法。 - [path - 演算法筆記](https://web.ntnu.edu.tw/~algo/Path.html) #### 戴克斯特拉演算法(Dijkstra's Algorithm) ![image](https://hackmd.io/_uploads/HyE20njxJx.png) 戴克斯特拉演算法是一種廣泛使用的方法,特別適用於權重非負的有向圖和無向圖,通過不斷選擇**當前距離最小的未訪問節點**,並更新其所有相鄰節點的最短距離,來尋找從起始節點到其他所有節點的最短路徑。 特別要注意的是,若存在負權重的邊,則戴克斯特拉演算法就不再適用。為什麼?讓我們看看以下的圖: ![image](https://hackmd.io/_uploads/BkrsVKGb1e.png) 你想到原因了嗎?在這張圖中,使用戴克斯特拉演算法會發生什麼事情? - [基礎演算法系列 — Graph 資料結構與 Dijkstra’s Algorithm](https://medium.com/%E6%8A%80%E8%A1%93%E7%AD%86%E8%A8%98/%E5%9F%BA%E7%A4%8E%E6%BC%94%E7%AE%97%E6%B3%95%E7%B3%BB%E5%88%97-graph-%E8%B3%87%E6%96%99%E7%B5%90%E6%A7%8B%E8%88%87dijkstras-algorithm-6134f62c1fc2) - [Visualization of Above Example](https://graphicmaths.com/computer-science/graph-theory/dijkstras-algorithm/) #### Bellman–Ford Algorithm 相較於戴克斯特拉演算法,Bellman–Ford Algorithm 可以用在含有負權重邊的情況,是一種能處理負權重邊的最短路徑演算法。 Bellman–Ford 的核心步驟是反覆對所有邊做 Relaxation(檢查是否可以利用某條邊縮短當前已知的最短路徑距離,若可以則更新),經過 V−1 次 Relaxation 的操作後(V 為節點數),可以找到從起點到其他所有節點的最短路徑。 雖然可以處理包含負權重邊的圖,但圖中不能存在負權重環(總和為負的環)。為什麼? > 若圖中存在負權重環,我們可以一直在這個環中不斷遍歷,就會造成最短路徑變為負無限大。 > 反過來說,若在 V−1 次放鬆後,還有可以縮短的路徑,則代表該圖中存在負權重環,因此最短路徑不存在。 > 因此,Bellman–Ford 不僅適用於包含負權重邊的圖,還可以檢測負權重環。 - [[演算法] 最短路徑 (Bellman-Ford 演算法)](https://ithelp.ithome.com.tw/articles/10209748) #### Floyd-Warshall Algorithm Floyd-Warshall 演算法用於解決所有點對最短路徑問題(All-Pairs Shortest Paths Problem),是一種基於動態規劃的方法。與 Dijkstra 和 Bellman-Ford 不同的是,Floyd-Warshall 可以計算算任意兩個節點之間的最短路徑。 其主要的想法是:假設我們要從節點 i 到節點 j 找最短路徑,若能經由中間節點 k 獲得更短的距離,那麼我們就更新該最短路徑。用遞迴公式表示如下:$d[i][j] = min(d[i][j], d[i][k] + d[k][j])$。因此我們總共需要做 V 次更新,每次都檢查各點對之間的距離是否有更佳解。 - [[演算法] 最短路徑 (Floyd-Warshall 演算法)](https://ithelp.ithome.com.tw/articles/10209186) #### Leetcode - [743. Network Delay Time](https://leetcode.com/problems/network-delay-time/description/) - [1334. Find the City With the Smallest Number of Neighbors at a Threshold Distance](https://leetcode.com/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/description/) <!-- - [787. Cheapest Flights Within K Stops](https://leetcode.com/problems/cheapest-flights-within-k-stops/description/) --> --- ### 生成樹(Spanning Tree) - [演算法筆記:Spanning Tree](https://web.ntnu.edu.tw/~algo/SpanningTree.html) 從一張圖取出一棵樹,樹上可以連接圖上所有點,就稱為該圖的生成樹,一個圖可能有不只一種生成樹。 ![image](https://hackmd.io/_uploads/S1zbFo8eC.png) #### 最小生成樹(Minimum Cost Spanning Tree) 顧名思義,最小生成樹就是最小的生成樹,這邊的最小指的是「連接所有節點所需的花費(所有邊的花費總和)最小」。 以下為兩個常見且易懂的貪婪演算法,來尋找圖中的最小生成樹: ##### Kruskal's algorithm 不斷從所有邊裡面選花費最小的邊,並判斷加入此邊可否連通兩個不同的 Component,直到所有的節點都被相連。(以所有邊作為每輪檢查的對象) ##### Prim's Algorithm 選定一個起點,挑選與其連接的邊中,花費最小且可連接兩個不同 Component 的相連,再把新連接的所有邊加入下一輪的檢查。其精神與 Dijkstra's Algorithm 有幾分類似。(以目前相連的點包含的所有邊,作為每輪檢查的對象) > 備註:也有所謂的最大生成樹,概念一模一樣 #### Leetcode - [1584. Min Cost to Connect All Points](https://leetcode.com/problems/min-cost-to-connect-all-points/description) --- ### 有向無環圖(Directed Acyclic Graph)與拓樸排序(Topological Sorting) - [演算法筆記:DAG](https://web.ntnu.edu.tw/~algo/DirectedAcyclicGraph.html) 有向無環圖(Directed Acyclic Graph)又稱 DAG,顧名思義,是有方向且不包含環的 Graph。 拓樸排序(Topological Sorting)是一種將 DAG 中的所有節點排序的方式,使得每條有向邊的排序符合其方向性(舉例來說:u->v->w 的拓樸排序為 [u, v, w])。因為 DAG 之中不會有環,故只要是 DAG 則必能找到一個拓樸排序。 ![image](https://hackmd.io/_uploads/B1txFj8gR.png) 以下介紹一個在 DAG 中尋找拓樸排序的方法: #### Kahn's Algorithm 透過找出圖中的 in-degree 為零的節點,逐步移除這些節點,並同時更新圖的 in-degree,從而達到拓樸排序。 - 若所有節點皆能被移除,則移除順序即為拓樸排序 - 若無法移除所有節點,則代表圖中有環(因此此圖不會是 DAG) #### Leetcode - [207. Course Schedule](https://leetcode.com/problems/course-schedule/description/) - [210. Course Schedule II](https://leetcode.com/problems/course-schedule-ii/description/) ## 差分陣列(Difference Array) ![image](https://hackmd.io/_uploads/HkVH0QvMyl.png) 差分陣列是一種快速處理**區間更新**的技巧,特別適用於頻繁對陣列特定區間進行加減的情境,其基於前綴和(Prefix Sum)的概念來輔助實現。 其核心概念是透過維護一個輔助陣列 diff,來記錄原陣列的數值變化,使得區間的加減可以在 O(1) 時間內完成,而最終結果則可透過前綴和的技巧來計算。 ### 輔助陣列的建立 輔助陣列 diff 的計算方法為: diff[0] = nums[0] diff[i] = nums[i] - nums[i-1] (i > 0) --- 以上圖來看,原陣列 nums 是 [8, 2, 6, 3, 1]: diff[0] = 8 diff[1] = nums[1] - nums[0] = 8 - 2 = -6 diff[2] = nums[2] - nums[1] = 6 - 2 = 4 ... diff = [8, -6, 4, -3, -2] 有注意到嗎?每個 diff 記錄的是「這個位置與上個位置的差異」。紀錄差異而不記錄實際數值,將會幫助我們更容易的更新整個區間。 ### 區間的更新 在建立完輔助陣列 diff 後,若要對原陣列 nums 在區間 [L, R] 內的所有元素加上 X,則執行: diff[L] += X(標記從 L 開始增加 X) diff[R+1] -= X(標記從 R+1 開始減少 X) 最終透過前綴和來還原結果: nums[i] = nums[i-1] + diff[i] --- 以上圖來看,若我們想在 [1, 3] 之間的所有元素增加 2: diff[1] += 2 diff[3] -= 2 diff = [8, -4, 4, -5, -2] 透過前綴和還原的結果為: nums2 = [8, 4, 8, 3, 1] 相比原本的結果: nums = [8, 2, 6, 3, 1] 是不是很神奇! - [小而美的算法技巧:差分陣列](https://labuladong.online/algo/data-structure/diff-array/) ### Leetcode - [1094. Car Pooling](https://leetcode.com/problems/car-pooling/description/) - [1109. Corporate Flight Bookings](https://leetcode.com/problems/corporate-flight-bookings/description/) ## 單調堆疊/佇列(Monotonic Stack/Queue) 單調堆疊和單調佇列是處理數列問題的工具,與一般的堆疊/佇列類似,但差異是我們在遍歷陣列時,會持續保持堆疊/佇列裡面的元素「有規律地排序」,比如從小到大或從大到小,而那些不符合順序的會將其剔除,並拿來做我們需要的運算。 一般來說,比較常使用到的是單調堆疊,因此在 Leetcode 上也比較有機會看到。單調佇列的概念基本上差不多,只差在移除元素的方法與應用情景不同。 <!-- ![image](https://hackmd.io/_uploads/rypgyhjgkx.png) --> <!-- - [演算法筆記系列 — Monotonic Stack/Queue](https://medium.com/%E6%8A%80%E8%A1%93%E7%AD%86%E8%A8%98/%E6%BC%94%E7%AE%97%E6%B3%95%E7%AD%86%E8%A8%98%E7%B3%BB%E5%88%97-monotonic-stack-queue-5ad1c35a3dfe) --> ### 單調堆疊(Monotonic Stack) 覺得上面有點複雜的話,我們可以來看看以下的例子: #### 單調遞增堆疊(Monotonically Increasing Stack) ![image](https://hackmd.io/_uploads/r1gLZiqBye.png) 在遞增的情境下,我們遍歷原始陣列,並將元素加入堆疊中。但若新加入的元素比堆疊頂端的元素還小,我們就將頂端元素取出,再做一次檢查,重複執行直到「單調遞增」的性質被滿足為止。 #### 單調遞減堆疊(Monotonically Decreasing Stack) ![image](https://hackmd.io/_uploads/ByJw-ocByl.png) #### 單調堆疊的應用 單調堆疊的主要應用是快速處理「最近相關」的問題,特別是數列中每個元素的上一個/下一個更大值/更小值。 假設數列 value = [6, 2, 5, 7],求每個數的「下一個更大值」。 * 初始化: * stack = [] # (index, value) of element * result = [-1, -1, -1, -1] # next greater element * 遍歷: * 6: stack = [(0, 6)], result = [-1, -1, -1, -1] * 2: stack = [(0, 6), (1, 2)], result = [-1, -1, -1, -1] * 5: stack = [(0, 6), (2, 5)], result = [-1, 5, -1, -1] * Remove (1, 2) (value[1] = 2 < value[2] = 5) * result[1] = value[2] = 5 * 7: stack = [(3, 7)], result = [7, 5, 7, -1] * Remove (2, 5) (value[2] = 5 < value[3] = 7) * result[2] = value[3] = 7 * Remove (0, 6) (value[0] = 6 < value[3] = 7) * result[0] = value[3] = 7 * 結果:[7, 5, 7, -1] - [參考:Monotonic Stack – 陪你刷題](https://haogroot.com/2020/09/01/monotonic-stack-leetcode/) #### Leetcode - [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/description/) - [496. Next Greater Element I](https://leetcode.com/problems/next-greater-element-i/description/) - [739. Daily Temperatures](https://leetcode.com/problems/daily-temperatures/description) ### 單調佇列(Monotonic Queue) 與單調堆疊類似,但單調佇列不是嚴格意義上的佇列。單調佇列允許從兩端移除元素,與一般佇列的只能進先出不同,這麼做的好處是方便我們處理特定的問題。 #### 單調佇列的應用 ![image](https://hackmd.io/_uploads/Syq69Jsrkg.png) 單調隊列的主要用途是處理「滑動窗口」問題。滑動窗口是指在數列中一段固定範圍內進行計算,像找最大值或最小值。 假設數列 value = [3, 2, -3, -1, 0],求各個滑動窗口(k = 3)的最大值。 * 初始化: * queue = [] # (index, value) of element * result = [] # sliding window maximum * 遍歷: * [3, 2, -3]: queue = [(0, 3), (1, 2), (2, -3)], result = [3] * The value of queue is monotonic * Add value[0] = 3 to result * [2, -3, -1]: queue = [(1, 2), (3, -1)], result = [3, 2] * Remove (2, -3) (value[2] = -3 < value[3] = -1) * Remove (0, -3) (exceeding boundary) * Add value[1] = 2 to result * [-3, -1, 0]: queue = [(4, 0)], result = [3, 1, 0] * Remove (3, -1) (value[3] = -1 < value[4] = 0) * Add (4, 0) in queue for this round * Remove 1 for exceeding boundary * Add value[4] = 0 to result * 結果:[3, 2, 0] - [參考:Sliding Window Maximum – Monotonic queue 的應用](https://bengersay.com/sliding-window-maximum/) #### Leetcode - [239. Sliding Window Maximum](https://leetcode.com/problems/sliding-window-maximum/) ### 為什麼一個可以從兩端移除,一個只能從一端? 為什麼會有這樣的差異? * 單調堆疊(Monotonic Stack): * 資料的處理是單方向的(例如從左到右或從右到左)。 * 只有最近加入的元素是相關的,因此僅需從頂部彈出即可。 * 單調隊列(Monotonic Queue): * 資料是動態處理的,通常涉及一個範圍(例如滑動視窗)。 * 兩端的元素都可能變得不相關:前端的元素可能因為超出視窗範圍而需要移除,而後端的元素可能因違反單調性而需要移除。 單調堆疊與單調佇列算是比較進階與特殊的資料結構,平常我們並不會常常使用,但在某些情況下,他們的單調性質可以很好的幫我們解決特定問題,因此也是值得學習的主題。 ## 最大子數列問題 (Maximum Subarray Problem) 最大子陣列問題是指在一個整數數列中,找出一段連續子陣列,使其元素總和最大。這是經典的動態規劃問題之一,常用於資料分析,如股價變動趨勢。 ![image](https://hackmd.io/_uploads/BJFh7JLZxx.png) - 例子:給定一個陣列 [-2, -3, 4, -1, -2, 1, 5, -3],找出總和最大的子陣列。 - 解答:最大子陣列為 [4, -1, -2, 1, 5],其總和為 7。 ### Kadane's Algorithm Kadane’s Algorithm 是一種專門用來解決此問題的演算法,可在線性時間內計算完成。要了解 Kadane's Algorithm,讓我們從暴力法一步一步來看。 - [滴滴面试手撕算法题-kadane算法](https://zhuanlan.zhihu.com/p/85188269) #### 暴力法:遍歷全部連續子陣列 最簡單的方式,就是窮舉所有可能的連續子陣列,並計算每一個子陣列的總和,再從中找出最大值。 ```python= def maxSubArrayBF(nums): max_sum = nums[0] n = len(nums) for i in range(n): for j in range(i+1, n+1): total = sum(nums[i:j]) max_sum = max(max_sum, total) return max_sum ``` - 時間複雜度:$O(n^3)$ - 空間複雜度:$O(1)$ 就算我們用小技巧 curr_sum,來優化 sum() 的計算,時間複雜度依舊很高。 ```python= def maxSubArrayBF2(nums): max_sum = nums[0] n = len(nums) for i in range(n): curr_sum = 0 for j in range(i, n): curr_sum += nums[j] max_sum = max(max_sum, curr_sum) return max_sum ``` - 時間複雜度:$O(n^2)$ - 空間複雜度:$O(1)$ #### 優化時間:動態規劃 如果我們知道「以某個位置結尾的最大子陣列和」,那我們就可以用它來推導下一個位置的最大值 -> 動態規劃! * dp[i]: 表示 以 index i 結尾 的最大連續子陣列和。 * dp[i] = max(dp[i-1], 0)+ nums[i] * 延續之前的子陣列(dp[i-1] + nums[i]) or 重新開始(nums[i]) * 若前面總和小於 0,則完全不取前面,重新計算 ```python= def maxSubArrayDP(nums): n = len(nums) dp = [0] * n dp[0] = nums[0] for i in range(1, n): dp[i] = max(dp[i-1], 0) + nums[i] return max(dp) ``` - 時間複雜度:$O(n)$ - 空間複雜度:$O(n)$ #### 優化空間:Kadane's Algorithm 如果我們仔細觀察動態規劃的解法,會發現其實根本不需要整個 dp 陣列,因為我們只關心上一個 dp[i-1] 和目前的值。因此,Kadane's Algorithm 利用以下兩個變數,來更進一步優化 dp 解法的空間複雜度: - local_max:目前位置為結尾的最大子陣列和 - global_max:迄今為止出現過的最大子陣列和 ```python= def maxSubArrayKadane(nums): local_max = global_max = nums[0] for i in range(1, len(nums)): local_max = max(local_max, 0) + nums[i] global_max = max(global_max, local_max) return global_max ``` - 時間複雜度:$O(n)$ - 空間複雜度:$O(1)$ 至此,最大子數列問題已經被我們用僅僅 6 行的程式碼,加上 $O(n)$ 的時間與 $O(1)$ 的空間解決了。Kadane's Algorithm 是不是很精美呢? ### Leetcode - [53. Maximum Subarray](https://leetcode.com/problems/maximum-subarray/description/) - [918. Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray) ## 背包問題(Knapsack Problem) ![image](https://hackmd.io/_uploads/SJHGJ6jgyx.png) 背包問題(Knapsack Problem)是最佳化中的經典問題之一。 該問題的基本形式是: - 給定一個背包,其容量為固定的正整數(C) - 給定 N 種物品,每個物品都有自己的重量(w)與價值(v) - 目標是在不超過背包容量的前提下,選擇若干物品,使得它們的總價值最大 這個問題常用來模擬像是資源分配、投資選擇、或是行程安排等情境,也是一個常出現在演算法課程和競賽中的經典題型。 - [Knapsack Problem - 演算法筆記](https://web.ntnu.edu.tw/~algo/KnapsackProblem.html) - [ADA Handout (DP), Vivian Chen, NTU CSIE](https://www.csie.ntu.edu.tw/~yvchen/f107-ada/doc/181011_DynamicProgramming2.pdf) 根據限制的種類,背包問題又可以分為以下幾種: ### 分數背包問題(Fractional Knapsack Problem) - 每樣物品可以被切割(可放部分物品) - 解法:用貪婪演算法,依據「單位價值」高到低排序放入背包 這個問題的解法很直覺,直接依據「單位價值」(v / w,也就是我們常說的 CP 值)的高低放入背包,直到背包裝滿為止。 ```python= def fractional_knapsack(weights, values, capacity): items = sorted(zip(weights, values), key=lambda x: x[1]/x[0], reverse=True) total_value = 0 for w, v in items: if capacity >= w: total_value += v capacity -= w else: total_value += v * (capacity / w) break return total_value ``` ### 0/1 背包問題(0/1 Knapsack Problem) - 每樣物品只能選一次(不可切割),也是最經典的背包問題 - 解法:用動態規劃,考慮每一項物品「選」或「不選」 0/1 背包問題是最常見也最經典的背包問題,基本的解法是用 n x C 的 2D DP 矩陣,來代表在不同情況下,背包能達到的最大 total value,計算完後 dp[n][C] 即為解答。 ![image](https://hackmd.io/_uploads/By0VJYKGgg.png) ```python= def knapsack_2d(weights, values, capacity): n = len(weights) dp = [[0] * (capacity + 1) for _ in range(n + 1)] for i in range(1, n + 1): for w in range(capacity + 1): if weights[i - 1] <= w: dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]) else: dp[i][w] = dp[i - 1][w] return dp[n][capacity] ``` 我們也可以再更優化成 1D DP 來節省空間,並同時維持方法的正確性。要注意的是,若僅用一個 DP Array,則在內層迴圈要以倒序方式,才不會讓在該輪更新的值影響到計算。 ```python= def knapsack_1d(weights, values, capacity): n = len(weights) dp = [0] * (capacity + 1) for i in range(n): for w in range(capacity, weights[i] - 1, -1): # 倒序,避免重複選 dp[w] = max(dp[w], dp[w - weights[i]] + values[i]) return dp[capacity] ``` ### 有限背包問題(Bounded Knapsack Problem) - 每樣物品有固定數量,可能可以選擇大於 1 次 - 解法:將物品拆成多個 0/1 物品,把問題轉化成 0/1 背包問題來解 ```python= def bounded_knapsack(weights, values, counts, capacity): expanded_weights = [] expanded_values = [] for i in range(len(weights)): for _ in range(counts[i]): expanded_weights.append(weights[i]) expanded_values.append(values[i]) return knapsack_1d(expanded_weights, expanded_values, capacity) ``` ### 無限背包問題(Unbounded Knapsack Problem) - 每樣物品可以選無限次 - 解法:動態規劃,不同於 0/1 背包,內層迴圈需順序處理 因為每種物品可以選無限次,我們就不必用倒序方法來避免重複選擇,直接在內層迴圈順序選取即可。 ```python= def unbounded_knapsack(weights, values, capacity): dp = [0] * (capacity + 1) for i in range(len(weights)): for w in range(weights[i], capacity + 1): # 順序,允許重複選 dp[w] = max(dp[w], dp[w - weights[i]] + values[i]) return dp[capacity] ``` ## 換錢問題(Coin Change Problem) ![image](https://hackmd.io/_uploads/rkR7YYtfgg.png) 換錢問題是一個經典的演算法問題,目的是在給定一組硬幣面額與一個目標金額的情況下,找出可以湊出該金額所需的最少硬幣數量。 這個問題常用於訓練動態規劃(Dynamic Programming)技巧,並在許多實際應用中具有重要意義,例如金融系統、自動販賣機與資源分配等。 - [Knapsack Problem#Coin Problem - 演算法筆記](https://web.ntnu.edu.tw/~algo/KnapsackProblem.html) - [Dynamic programming 深入淺出 - 以 Coin change 為例](https://medium.com/@cutesciuridae/dynamic-programming-%E6%B7%B1%E5%85%A5%E6%B7%BA%E5%87%BA-%E4%BB%A5coin-change%E7%82%BA%E4%BE%8B-4a4f3e7d98ea) 利用這個問題,我們順便來複習一下動態規劃(Dynamic Programming): ![image](https://hackmd.io/_uploads/rJby5FtGex.png) 以這個問題為例: 1. 定義狀態 [我在哪裡] DP[n] =找出n元最精簡的找零零錢數目(用最少數目的銅板湊出) 2. 定義狀態轉移關係式(通則) [我從哪裡來] => [答案從哪裡推導而來] ![image](https://hackmd.io/_uploads/HkegB9YFfgl.png) 3. 釐清初始狀態(終止條件) [第一步怎麼走,怎麼出發的] 用每個銅板去縮小找零問題的規模,從n元找零問題一直化簡,降到0元的找 終止狀態: 0元的找零方法:0,代表不拿任何一枚銅板 <0元的找零方法:不合法,應該停止計算,可用 float('inf') 來處理 ### Leetcode - [322. Coin Change](https://leetcode.com/problems/coin-change/description/) <!-- - [518. Coin Change II](https://leetcode.com/problems/coin-change-ii/description/) --> <!-- ## 旅行銷售員問題(TSP, Traveling Salesman Problem) ![image](https://hackmd.io/_uploads/Sk8U16sekl.png) --> <!-- # Progress check ## 講義 - TODO: 尋找更多主題 - TODO: Update to blog ## 學生 - 進階演算法: LC 42, 239 not finished, Kadane finish -->

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully