【算法笔记】《labuladong 的算法小抄》第 1 章:核心套路篇之动态规划

写在本书之前

本书约定

  1. 一切以可读性为目标:Python、C++ 和 Java 混用
  2. 最小化语言特性,专注算法思维:使用内置数据结构


数据结构

LeetCode

  1. 二叉树节点 TreeNode
  2. 单链表节点 ListNode

C++

函数参数默认传值:& 引用容器

  1. 动态数组 vector :避免从其中间或头部增删元素的低效操作
  2. 字符串 string :直接用 if(s1 == s2) 判断相等
  3. 哈希表 unordered_map
    • 键一般为 intstring 类型
    • 方括号 [] 访问键时,若键不存在,则自动创建
  4. 哈希集合 unordered_set
  5. 队列 queue
  6. 堆栈 stack
    • pop 方法是 void 类型的,需要先存待删除元素

Java

  1. 数组 :非空检查
  2. 字符串 string
    • 不能直接修改,要用 toCharArray 转化成 char[] 类型数组再修改
    • + 拼接效率低,推荐使用 StringBuilderappend 方法
    • if(s1.equals(s2)) 判断相等
  3. 动态数组 ArrayList
  4. 双链表 LinkedList
  5. 哈希表 HashMap
  6. 哈希集合 HashSet
  7. 队列 queueQueue<String> q = new LinkedList<>()
  8. 堆栈 stack

Python 3

  1. 列表 list :数组、堆栈和队列
  2. 元组 tuple
  3. 字典 dict
    • 动态规划的备忘录memo = dict(), memo[(i, j)]


第 1 章 / 核心套路

  • 算法
    • 通过合适的工具数据结构
    • 解决特定问题的方法


1.1. 框架思维

  • 整体细节
  • 抽象具体


1.1.1. 数据结构的存储方式

  • 数组(顺序存储
    • 随机访问
    • 复制扩容
    • 移动插入删除
  • 链表(链式存储
    • 顺序访问
    • 插入扩容
    • 直接插入删除
数组 链表
队列/栈 需处理扩容和缩容 需存储节点指针
邻接矩阵 邻接表(稀疏矩阵)
哈希表(处理哈希冲突) 线性探查法 拉链法
堆(完全二叉树) 二叉搜索树、AVL 树、红黑树、区间树、B 树等


1.1.2. 数据结构的基本操作

数据结构的使命

  • 在不同的应用场景下尽可能 高效增、删、查、改

遍历 + 访问

  • 线性for/while 迭代

    • 数组迭代 遍历框架
    void traverse(int [] arr) {
        for (int i = 0; i < arr.length; i++) {
            // 迭代遍历 arr[i]
        }
    }
    
    • 链表迭代 遍历框架
    /* 单链表节点 */
    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
    }
    
    • 二叉树递归 遍历框架
    /* 二叉树节点 */
    class TreeNode {
      int val;
      TreeNode left, right;
    }
    
    void traverse(TreeNode root) {
      // 前序遍历(根左右)
      traverse(root.left);
      // 中序遍历(左根右)
      traverse(root.right);
      // 后序遍历(左右根)
    }
    
    • N 叉树递归 遍历框架 → 的遍历(多个 N 叉树 + 布尔数组 visited
    /* N 叉树节点 */
    class TreeNode {
      int val;
      TreeNode[] children; 
    }
    
    void traverse(TreeNode root) {
      for (TreeNode child : root.children) {
        traverse(child);
      }
    }
    

算法刷题第一步:二叉树

  • 最容易培养框架思维
  • 大部分常考算法(回溯、动态规划、分治)本质上是树的遍历问题(递归


1.2. 动态规划解题套路框架

  • 一般形式:求最值(如 最长 递增子序列、最小 编辑距离)

  • 核心问题:穷举
  • 动态规划三要素
    • 存在“重叠子问题”:优化穷举效率(备忘录、DP table)
    • 具备“最优子结构”:子问题的最值 → 原问题的最值
    • 正确的“状态转移方程
      • 问题的 base case(最简单情况
      • 状态”空间
      • 选择”使“状态”改变
      • dp 数组表示“状态”和“选择”
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态 1 in 状态 1 的所有取值:
    for 状态 2 in 状态 2 的所有取值:
        for ...
            dp[状态 1][状态 2][...] = 求最值 (选择 1, 选择 2, ...)


1.2.1. 斐波那契数列:重叠子问题

斐波那契数列: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. 递归算法递归树:时间复杂度 O(2N),空间复杂度 O(1)
递归算法的递归树

↓“剪枝”

  1. 带“备忘录”的递归算法(自顶向下)递归图:时间复杂度 O(N),空间复杂度 O(N)
带“备忘录”的递归算法的递归图

  1. dp 数组的迭代算法(自底向上)DP table 图:时间复杂度 O(N),空间复杂度 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];
}

状态转移方程

状态转移方程

状态压缩dp 数组的迭代算法:时间复杂度 O(N),空间复杂度 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. 凑零钱问题:状态转移方程

k 种面值的硬币:c1, c2, ..., ck,以最少的硬币数凑出总金额 amount

算法的函数签名

// coins 可选硬币面值,amount 目标金额
int coinChange(int[] coins, int amount);
  1. 暴力递归:确定状态转移方程
    • 时间复杂度 O(knk),空间复杂度 O(1)
    1. 确定 base case:amount 为 0 时,算法返回 0
    2. 确定“状态”:amount
    3. 确定“选择”:coins
    4. 确定 dp 函数/数组的定义:输入 amount,输出凑出 amount 的最少硬币数量
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;
    // 取子问题中各面值的最少硬币数
    res = Math.min(res, 1 + subproblem);
  }
  // 原问题不能凑出,则返回 -1
  return (res != Integer.MAX_VALUE) ? res : -1;
}

结果:递归超时

  1. 带“备忘录”的递归:自顶向下,消除重叠子问题
    • 时间复杂度 O(kn),空间复杂度 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;
    // 取子问题中各面值的最少硬币数
    res = Math.min(res, 1 + subproblem);
  }
  // 原问题不能凑出,则设置备忘录值为 -1
  memo[n] = (res != Integer.MAX_VALUE) ? res : -1;
  return memo[n];
}
  1. dp 数组的迭代:自底向上
    • 时间复杂度 O(kn),空间复杂度 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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容