【算法筆記】《labuladong 的算法小抄》第 1 章:核心套路篇之動態(tài)規(guī)劃

寫在本書之前

本書約定

  1. 一切以可讀性為目標(biāo):Python、C++ 和 Java 混用
  2. 最小化語言特性,專注算法思維:使用內(nèi)置數(shù)據(jù)結(jié)構(gòu)


數(shù)據(jù)結(jié)構(gòu)

LeetCode

  1. 二叉樹節(jié)點 TreeNode
  2. 單鏈表節(jié)點 ListNode

C++

函數(shù)參數(shù)默認(rèn)傳值:& 引用容器

  1. 動態(tài)數(shù)組 vector :避免從其中間或頭部增刪元素的低效操作
  2. 字符串 string :直接用 if(s1 == s2) 判斷相等
  3. 哈希表 unordered_map
    • 鍵一般為 intstring 類型
    • 方括號 [] 訪問鍵時,若鍵不存在,則自動創(chuàng)建
  4. 哈希集合 unordered_set
  5. 隊列 queue
  6. 堆棧 stack
    • pop 方法是 void 類型的,需要先存待刪除元素

Java

  1. 數(shù)組 :非空檢查
  2. 字符串 string
    • 不能直接修改,要用 toCharArray 轉(zhuǎn)化成 char[] 類型數(shù)組再修改
    • + 拼接效率低,推薦使用 StringBuilderappend 方法
    • if(s1.equals(s2)) 判斷相等
  3. 動態(tài)數(shù)組 ArrayList
  4. 雙鏈表 LinkedList
  5. 哈希表 HashMap
  6. 哈希集合 HashSet
  7. 隊列 queueQueue<String> q = new LinkedList<>()
  8. 堆棧 stack

Python 3

  1. 列表 list :數(shù)組、堆棧和隊列
  2. 元組 tuple
  3. 字典 dict
    • 動態(tài)規(guī)劃的備忘錄memo = dict(), memo[(i, j)]


第 1 章 / 核心套路

  • 算法
    • 通過合適的工具數(shù)據(jù)結(jié)構(gòu)
    • 解決特定問題的方法


1.1. 框架思維

  • 整體細(xì)節(jié)
  • 抽象具體


1.1.1. 數(shù)據(jù)結(jié)構(gòu)的存儲方式

  • 數(shù)組(順序存儲
    • 隨機訪問
    • 復(fù)制擴容
    • 移動插入刪除
  • 鏈表(鏈?zhǔn)酱鎯?/em>
    • 順序訪問
    • 插入擴容
    • 直接插入刪除
數(shù)組 鏈表
隊列/棧 需處理擴容和縮容 需存儲節(jié)點指針
鄰接矩陣 鄰接表(稀疏矩陣)
哈希表(處理哈希沖突) 線性探查法 拉鏈法
堆(完全二叉樹) 二叉搜索樹、AVL 樹、紅黑樹、區(qū)間樹、B 樹等


1.1.2. 數(shù)據(jù)結(jié)構(gòu)的基本操作

數(shù)據(jù)結(jié)構(gòu)的使命

  • 在不同的應(yīng)用場景下盡可能 高效增、刪、查、改

遍歷 + 訪問

  • 線性for/while 迭代

    • 數(shù)組迭代 遍歷框架
    void traverse(int [] arr) {
        for (int i = 0; i < arr.length; i++) {
            // 迭代遍歷 arr[i]
        }
    }
    
    • 鏈表迭代 遍歷框架
    /* 單鏈表節(jié)點 */
    class ListNode {
      int val;
      ListNode next;
    }
    
    void traverse(ListNode head) {
      for (ListNode p = head; p != null; p = p.next) {
        // 迭代遍歷 p.val
      }
    }
    
  • 非線性遞歸

    • 鏈表遞歸 遍歷框架
    void traverse(ListNode head) {
      // 前序遍歷 head.val
      traverse(head.next);
      // 后序遍歷 head.val
    }
    
    • 二叉樹遞歸 遍歷框架
    /* 二叉樹節(jié)點 */
    class TreeNode {
      int val;
      TreeNode left, right;
    }
    
    void traverse(TreeNode root) {
      // 前序遍歷(根左右)
      traverse(root.left);
      // 中序遍歷(左根右)
      traverse(root.right);
      // 后序遍歷(左右根)
    }
    
    • N 叉樹遞歸 遍歷框架 → 的遍歷(多個 N 叉樹 + 布爾數(shù)組 visited
    /* N 叉樹節(jié)點 */
    class TreeNode {
      int val;
      TreeNode[] children; 
    }
    
    void traverse(TreeNode root) {
      for (TreeNode child : root.children) {
        traverse(child);
      }
    }
    

算法刷題第一步:二叉樹

  • 最容易培養(yǎng)框架思維
  • 大部分??妓惴ǎ?strong>回溯、動態(tài)規(guī)劃、分治)本質(zhì)上是樹的遍歷問題(遞歸


1.2. 動態(tài)規(guī)劃解題套路框架

  • 一般形式:求最值(如 最長 遞增子序列、最小 編輯距離)

  • 核心問題:窮舉
  • 動態(tài)規(guī)劃三要素
    • 存在“重疊子問題”:優(yōu)化窮舉效率(備忘錄、DP table)
    • 具備“最優(yōu)子結(jié)構(gòu)”:子問題的最值 → 原問題的最值
    • 正確的“狀態(tài)轉(zhuǎn)移方程
      • 問題的 base case(最簡單情況
      • 狀態(tài)”空間
      • 選擇”使“狀態(tài)”改變
      • dp 數(shù)組表示“狀態(tài)”和“選擇”
# 初始化 base case
dp[0][0][...] = base case
# 進(jìn)行狀態(tài)轉(zhuǎn)移
for 狀態(tài) 1 in 狀態(tài) 1 的所有取值:
    for 狀態(tài) 2 in 狀態(tài) 2 的所有取值:
        for ...
            dp[狀態(tài) 1][狀態(tài) 2][...] = 求最值 (選擇 1, 選擇 2, ...)


1.2.1. 斐波那契數(shù)列:重疊子問題

斐波那契數(shù)列:1, 1, 2, 3, 5, 8, ...

int fib(int N) {
  if (N == 0) return 0;
  if (N == 1 || N == 2) return 1;
  return fib(N - 1) + fib(N - 2);
}
  1. 遞歸算法遞歸樹:時間復(fù)雜度 O(2N),空間復(fù)雜度 O(1)
遞歸算法的遞歸樹

↓“剪枝”

  1. 帶“備忘錄”的遞歸算法(自頂向下)遞歸圖:時間復(fù)雜度 O(N),空間復(fù)雜度 O(N)
帶“備忘錄”的遞歸算法的遞歸圖

  1. dp 數(shù)組的迭代算法(自底向上)DP table 圖:時間復(fù)雜度 O(N),空間復(fù)雜度 O(N)
DP table
int fib(iny N) {
  if (N == 0) return 0;
  if (N == 1 || N == 2) return 1;
  vector<int> dp(N + 1, 0);
  // base case
  dp[1] = dp[2] = 1;
  for (int i = 3; i <= N; i++) {
    dp[i] = dp[i - 1] + dp[i - 2];
  }
  return dp[N];
}

狀態(tài)轉(zhuǎn)移方程

狀態(tài)轉(zhuǎn)移方程

狀態(tài)壓縮dp 數(shù)組的迭代算法:時間復(fù)雜度 O(N),空間復(fù)雜度 O(1)

int fib(int N) {
  if (N == 0) return 0;
  if (N == 1 || N == 2) return 1;
  int pre2 = 1, pre1 = 1;
  for (int i = 3; i <= N; i++) {
    int cur = pre2 + pre1;
    pre2 = pre1;
    pre1 = cur;
  }
  return pre1;
}


1.2.2. 湊零錢問題:狀態(tài)轉(zhuǎn)移方程

k 種面值的硬幣:c1, c2, ..., ck,以最少的硬幣數(shù)湊出總金額 amount

算法的函數(shù)簽名

// coins 可選硬幣面值,amount 目標(biāo)金額
int coinChange(int[] coins, int amount);
  1. 暴力遞歸:確定狀態(tài)轉(zhuǎn)移方程
    • 時間復(fù)雜度 O(knk),空間復(fù)雜度 O(1)
    1. 確定 base case:amount 為 0 時,算法返回 0
    2. 確定“狀態(tài)”:amount
    3. 確定“選擇”:coins
    4. 確定 dp 函數(shù)/數(shù)組的定義:輸入 amount,輸出湊出 amount 的最少硬幣數(shù)量
int coinChange(int[] coins, int amount) {
  return dp(amount, coins);
}

int dp(int n, int[] coins) {
  // base case
  if (n == 0) return 0;
  if (n < 0) return -1;

  int res = Integer.MAX_VALUE;
  for (int coin : coins) {
    int subproblem = dp(n - coin, coins);
    // 子問題不能湊出,跳過這一面值
    if (subproblem == -1) continue;
    // 取子問題中各面值的最少硬幣數(shù)
    res = Math.min(res, 1 + subproblem);
  }
  // 原問題不能湊出,則返回 -1
  return (res != Integer.MAX_VALUE) ? res : -1;
}

結(jié)果:遞歸超時

  1. 帶“備忘錄”的遞歸:自頂向下,消除重疊子問題
    • 時間復(fù)雜度 O(kn),空間復(fù)雜度 O(n)
int coinChange(int[] coins, int amount) {
  int[] memo = new int[amount + 1];
  Arrays.fill(memo, 0);
  return helper(memo, amount, coins);
}

int helper(int[] memo, int n, int[] coins) {
  // base case
  if (n == 0) return 0;
  if (n < 0) return -1;

  if (memo[n] != 0) return memo[n];

  int res = Integer.MAX_VALUE;
  for (int coin : coins) {
    int subproblem = helper(memo, n - coin, coins);
    // 子問題不能湊出,則跳過這一面值
    if (subproblem == -1) continue;
    // 取子問題中各面值的最少硬幣數(shù)
    res = Math.min(res, 1 + subproblem);
  }
  // 原問題不能湊出,則設(shè)置備忘錄值為 -1
  memo[n] = (res != Integer.MAX_VALUE) ? res : -1;
  return memo[n];
}
  1. dp 數(shù)組的迭代:自底向上
    • 時間復(fù)雜度 O(kn),空間復(fù)雜度 O(n)
int coinChange(int[] coins, int amount) {
  if (amount == 0) return 0;
  if (amount < 0) return -1;
  
  int[] dp = new int[amount + 1];
  // 初始化為 amount + 1,最多用 amount + 1 枚硬幣湊出
  Arrays.fill(dp, amount + 1);

  // base case
  dp[0] = 0;
  for (int i = 1; i <= amount; i++) {
    for (int coin : coins) {
      // 子問題超出范圍,跳過
      if (i - coin < 0) continue;
      dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
    }
  }
  return (dp[amount] != amount + 1) ? dp[amount] : -1;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容