当前位置: 首页 > news >正文

LeetCode算法日记 - Day 66: 衣橱整理、斐波那契数(含记忆化递归与动态规划总结)

目录

1. 衣橱整理

1.1 题目解析

1.2 解法

1.3 代码实现

2. 斐波那契数

2.1 题目解析

2.2 解法

2.3 代码实现


1. 衣橱整理

https://leetcode.cn/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/

家居整理师将待整理衣橱划分为 m x n 的二维矩阵 grid,其中 grid[i][j] 代表一个需要整理的格子。整理师自 grid[0][0] 开始 逐行逐列 地整理每个格子。

整理规则为:在整理过程中,可以选择 向右移动一格 或 向下移动一格,但不能移动到衣柜之外。同时,不需要整理 digit(i) + digit(j) > cnt 的格子,其中 digit(x) 表示数字 x 的各数位之和。

请返回整理师 总共需要整理多少个格子

示例 1:

输入:m = 4, n = 7, cnt = 5
输出:18

提示:

  • 1 <= n, m <= 100
  • 0 <= cnt <= 20

1.1 题目解析

题目本质
这是一个图的可达性遍历问题。本质是从起点 (0,0) 出发,在满足约束条件下,统计所有能够到达的格子数量。

常规解法
最直观的想法是遍历整个 m×n 矩阵的每个格子,判断每个格子 (i,j) 是否满足 digit(i) + digit(j) ≤ cnt,满足条件就计数。

问题分析
常规解法忽略了一个关键约束——只能从 (0,0) 开始,且只能向右或向下移动。这意味着即使某个格子 (i,j) 满足数位和条件,但如果从起点无法到达它(路径上被其他不满足条件的格子阻断),也不能计入结果。例如:如果 (1,0) 不满足条件,那么第一列所有 i>0 的格子都无法到达。

思路转折
要想准确计数 → 必须考虑可达性 → 使用 DFS/BFS 从起点搜索。关键点:

  • 从 (0,0) 开始探索

  • 每次只能向右或向下移动

  • 访问新格子前先判断数位和条件

  • 用 vis 数组避免重复访问

1.2 解法

算法思想

使用深度优先搜索(DFS)从起点 (0,0) 开始遍历,每个格子只能向右 (i, j+1) 或向下 (i+1, j) 移动。对于新格子 (x, y),当且仅当满足以下条件时才访问:

  • 边界条件:0 ≤ x < m, 0 ≤ y < n

  • 未访问过:vis[x][y] == false

  • 数位和条件:digit(x) + digit(y) ≤ cnt

数位和计算公式:digit(x) = Σ(x 的每一位数字),例如 digit(13) = 1 + 3 = 4。

i)初始化:创建 vis[m][n] 访问标记数组,result 计数器置 0

ii)DFS 递归过程:

  • 标记当前格子为已访问:vis[si][sj] = true

  • 将当前格子计入结果:result++

  • 遍历两个方向(右、下):

    • 计算新坐标 (x, y)

    • 判断边界、访问状态、数位和条件

    • 满足条件则递归调用 dfs(x, y)

iii)数位和计算:循环提取数字的每一位(num % 10),累加后除以 10

iv)返回结果:DFS 结束后返回 result

易错点

  • 方向数组错误:题目要求"向右或向下",应该是 dx={1,0}, dy={0,1},不要写成 {1,-1} 或 {0,1}, {1,0} 等错误组合

  • 数位和计算错误:必须用 num % 10 取个位数字

  • 起点未计数:必须在 DFS 开始时对当前格子计数(result++),不能只在递归后计数,否则会漏掉起点

  • 重复标记:在递归调用前就要标记 vis[x][y] = true(或在 DFS 入口标记),避免重复访问

  • 边界判断顺序:先判断边界条件,再判断 vis 和数位和,避免数组越界

1.3 代码实现

class Solution {int[] dx = {1, 0};   // 向下、向右int[] dy = {0, 1};int m, n, cnt;boolean[][] vis;int result;public int wardrobeFinishing(int _m, int _n, int _cnt) {m = _m;n = _n;cnt = _cnt;vis = new boolean[m][n];result = 0;dfs(0, 0);return result;}public void dfs(int si, int sj) {vis[si][sj] = true;result++;  // 计数当前格子for (int i = 0; i < 2; i++) {int x = si + dx[i];int y = sj + dy[i];// 边界检查 + 访问检查 + 数位和检查if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y]) {int sum = get(x) + get(y);if (sum <= cnt) {dfs(x, y);  // 递归访问}}}}// 计算数字各位数字之和public int get(int num) {int sum = 0;while (num > 0) {sum += num % 10;  // 取个位num = num / 10;   // 去掉个位}return sum;}
}

复杂度分析

  • 时间复杂度:O(m × n)。每个格子最多访问一次,计算数位和的复杂度为 O(log num),由于 m, n ≤ 100,数位和计算可视为 O(1),总体最多访问 m×n 个格子

  • 空间复杂度:O(m × n)。vis 数组占用 O(m × n) 空间,递归调用栈最深为 O(m + n),在最坏情况下沿着边界走到 (m-1, n-1),总空间复杂度为 O(m × n)

2. 斐波那契数

https://leetcode.cn/problems/fibonacci-number/description/

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30

2.1 题目解析

题目本质
这是一个经典的递推序列计算问题,本质是"根据已知项推导第n项"。每一项都依赖前两项,形成一个链式依赖关系。

常规解法
最直观的想法就是直接按照定义写递归:F(n) = F(n-1) + F(n-2)。看起来代码简洁优雅,直接翻译数学定义。

问题分析
但直接递归会产生大量重复计算。比如计算F(5)时,F(3)会被计算多次(通过F(5)→F(4)→F(3)和F(5)→F(3)两条路径)。时间复杂度是O(2^n),当n=30时会有上亿次计算,完全无法接受。

思路转折:要想高效 → 必须避免重复计算 → 有三条路:

  • 记忆化递归:用数组缓存已计算的值,每个F(i)只算一次

  • 动态规划:自底向上迭代,从F(0)、F(1)开始逐步推到F(n)

  • 空间优化:发现每次只需要前两个数,可以用滚动变量代替数组

这样就能把O(2^n)降到O(n),把指数级灾难变成线性小菜一碟。

2.2 解法

算法思想
记忆化递归(Memoization),用一个数组memo存储已计算过的斐波那契数。递归时先查表,命中则直接返回;未命中则计算后存入表中。核心公式:

F(0) = 0, F(1) = 1
F(n) = F(n-1) + F(n-2)  (n > 1)

i)初始化备忘录数组memo[31],所有值填充为-1(表示未计算)

ii)递归函数dfs(n):

  • 边界条件:如果n是0或1,直接返回n

  • 查表:如果memo[n] != -1,说明已计算过,直接返回缓存值

  • 递归计算:memo[n] = dfs(n-1) + dfs(n-2)

  • 返回结果

易错点:

  • 数组大小要开到n的最大值+1(这里是31),否则索引越界

  • 初始化标记值要用-1而不是0,因为F(0)本身就是0,会导致混淆

  • 边界条件要同时处理n=0和n=1,缺一不可

2.3 代码实现

class Solution {int[] memo;public int fib(int n) {memo = new int[31];Arrays.fill(memo, -1); return dfs(n);}public int dfs(int n){if(n == 0 || n ==1) return n;if(memo[n] != -1){return memo[n];}memo[n] = dfs(n-1) + dfs(n-2);return memo[n];}
}

复杂度分析:

  • 时间复杂度:O(n)。每个斐波那契数F(i)只会计算一次,共n+1个数。

  • 空间复杂度:O(n)。备忘录数组占用O(n)空间,递归调用栈深度为O(n)。

3. 记忆化递归 vs 动态规划

一体两面的优化艺术、同根同源,两者本质上是完全相同的优化思想——都是通过"空间换时间"来避免重复计算。核心都是:

  • 识别出子问题的重叠性

  • 用表格(数组/哈希表)存储已计算的结果

  • 后续遇到相同子问题直接查表返回

可以说:记忆化递归 = 递归 + 缓存,动态规划 = 迭代 + 缓存。它们只是实现形式不同,算法本质完全一致。

核心区别:自顶向下 vs 自底向上

记忆化递归(Memoization)动态规划(DP)
执行方向自顶向下(Top-Down)自底向上(Bottom-Up)
思维方式从目标出发,递归分解问题从基础情况出发,逐步构建
计算范围只计算需要的子问题(按需计算)计算所有子问题(全面覆盖)
实现方式递归函数 + 备忘录数组循环迭代 + DP数组
调用栈有递归栈开销(O(n)深度)无递归栈开销
代码风格更接近数学定义,直观需要理解状态转移,稍抽象

形象理解:

  • 记忆化递归:像从山顶往下走,遇到岔路就分叉探索,走过的路做标记不再重复。代码直观易写,按需计算省空间,但有递归栈开销风险。

  • 动态规划:像从山脚往上爬,一步步搭建台阶,最终到达山顶。性能更优更稳定,易于空间优化,但需要提前设计状态转移逻辑。

http://www.dtcms.com/a/457617.html

相关文章:

  • 建行官方网站网站模块数据同步
  • HTTP 协议的基本格式
  • 【代码】洛谷 P6150 [USACO20FEB] Clock Tree S [思维]
  • 专业做网站的公司哪家好西宁网站建设公司
  • 信息安全基础知识:06认证技术
  • 哪一个网站做专栏作家好点橙色企业网站模板
  • 【区间DP】戳气球 题解
  • Ventoy下载和安装教程(图文并茂,非常详细)
  • 无向图的回路检测(广度优先并查集)
  • 磁悬浮轴承损耗:深度解析损耗机理与降耗之道
  • AI大模型赋能药物研发:破解“双十困局”的跨界革命
  • 哲林高拍仪网站开发宁波南部商务区网站建设
  • 经典的逻辑函数化简算法 Espresso
  • ZKEACMS:基于ASP.Net Core开发的开源免费内容管理系统
  • 【QT常用技术讲解】opencv实现摄像头图像检测并裁剪物体
  • 深圳建网站哪个好网页设计实训总结3000字大学篇
  • 【密码学实战】openHiTLS mac命令行:消息认证码工具
  • 沙井网站建设石林县工程建设个体交易网站
  • Chromium 138 编译指南 - Android 篇:安装构建依赖项(七)
  • asp 绿色环保企业网站源码 v1.1golang 做网站
  • 第4章 C++多线程系统编程精要
  • 绵阳网站怎样做网站设计要交税吗
  • 同ip网站有什么影响网站建设论文500字
  • 基于springboot的家具商城销售系统
  • 手机版网站制作佛山建站软件
  • 厦门南希网站建设微信应用程序开发
  • 【STM32项目开源】基于STM32的智能厨房火灾燃气监控
  • 最新彩虹云商城二开Pro美化版 新增超多功能 全开源
  • 如何制作家具网站东莞设计网站建设方案
  • 商丘做网站的电话怎样建网站最快