Hello 算法:“走一步看一步”的智慧

作者:灵感__idea日期:2026/4/26

每个系列一本前端好书,帮你轻松学重点。

本系列来自上海交通大学硕士,华为高级算法工程师 靳宇栋《Hello,算法》

“走一步看一步”,是我们面对不断变化的世界所采取的应对策略。

多数时候,我们无法对未来做出准确预测,只能根据上一件事的结果对下一件事做决策。介绍“分治”的时候,我们已经接触过这种策略。本篇主角依然如此,但又有所不同。

先看个例子。

爬楼梯

给一个 n 阶楼梯,每步可以上 1 阶或者 2 阶,问有多少种方案可以爬到楼顶?

假设 n 是3,那么方案共 3 种。如下图所示。

微信图片_2026-04-27_004502_056.jpg

这种方案采用的是“穷举”,每轮选择上 1 阶或 2 阶,每当到达顶部时就将方案数量加 1,当越过顶部时就将其剪枝。

这是一种将问题看做一系列决策步骤的方案,我们还可以尝试从问题分解的角度分析。

暴力搜索

假设,爬到第 i 阶有 dp(i)种方案,那么dp(i)就是原问题,子问题包括:

www.hello-algo.com_chapter_dynamic_programming_intro_to_dynamic_programming_.png

由于每轮只能上 1 阶或 2 阶,因此,我们只能从第 i-1阶或第 i-2 阶迈向第 i 阶。

可得出一个重要推论:爬到第 i -2 阶的方案数加上爬到第 i-1 阶的方案数就等于爬到第 i 阶的方案数。

www.hello-algo.com_chapter_dynamic_programming_intro_to_dynamic_programming_ (1).png

这意味着在爬楼梯问题中,各个子问题之间存在递推关系,原问题的解可以由子问题的解构建得来。如下图所示:

微信图片_2026-04-27_004511_440.jpg

代码实现:

1/* 搜索 */
2function dfs(i) {
3    // 已知 dp[1]  dp[2] ,返回之
4    if (i === 1 || i === 2) return i;
5    // dp[i] = dp[i-1] + dp[i-2]
6    const count = dfs(i - 1) + dfs(i - 2);
7    return count;
8}
9
10/* 爬楼梯:搜索 */
11function climbingStairsDFS(n) {
12    return dfs(n);
13}
14

看到一个熟悉的身影—“递归”,暴力搜索会形成一个递归树,对于问题 dp[n],其递归树的深度为 n,时间复杂度为O(2n)。

情况不太妙,因为指数阶具备爆炸式增长的特点,如果输入一个比较大的 n ,会陷入漫长的等待。

时间复杂度为何如此之高?我们观察一下递归树。

微信图片_2026-04-27_004517_283.jpg

聪明的你一眼就看出,这棵树出现了多个相同的“子问题”,大部分计算资源都浪费在这些重叠的子问题上。

那么,优化的措施就有了。

记忆化搜索

为提升效率,需要做的改进是:重叠子问题只计算一次

为此,我们声明一个数组 mem 来记录子问题的解,并在搜索过程中将重叠子问题剪枝。

  1. 首次计算 dp[i] 时,将其记录至 mem[i] ,以便之后使用。
  2. 再次需要计算 dp[i] 时,直接从 mem[i] 中获取结果,避免重复计算。

代码实现:

1/* 记忆化搜索 */
2function dfs(i, mem) {
3    // 已知 dp[1]  dp[2] ,返回之
4    if (i === 1 || i === 2) return i;
5    // 若存在记录 dp[i] ,则直接返回之
6    if (mem[i] != -1) return mem[i];
7    // dp[i] = dp[i-1] + dp[i-2]
8    const count = dfs(i - 1, mem) + dfs(i - 2, mem);
9    // 记录 dp[i]
10    mem[i] = count;
11    return count;
12}
13
14/* 爬楼梯:记忆化搜索 */
15function climbingStairsDFSMem(n) {
16    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录
17    const mem = new Array(n + 1).fill(-1);
18    return dfs(n, mem);
19}
20

简化后,所有重复子问题都只计算1次,时间复杂度为O(n),这是一个巨大的飞跃。

那么,记忆化搜索是否还存在短板,有继续优化的可能吗?

细心的朋友发现了,记忆化搜索“基于递归”的,这意味着:

1、每个子问题都需要一次函数调用,函数调用需要时间成本;

2、当数据量很大时,可能产生调用栈溢出;

怎么办?

动态规划

记忆化搜索流程是“始于原问题,分解成小问题”,通过回溯收集子问题的解,构建原问题的解。可称为 “从顶至底”。

与之相反,动态规划是一种 “从底至顶” 的方法,“从最小子问题的解开始,迭代地构建更大子问题的解”,直至得到原问题的解。

由于动态规划不包含“回溯”过程,就无须使用递归,只需循环迭代。

代码实现:

1/* 爬楼梯:动态规划 */
2function climbingStairsDP(n) {
3    if (n === 1 || n === 2) return n;
4    // 初始化 dp 表,用于存储子问题的解
5    const dp = new Array(n + 1).fill(-1);
6    // 初始状态:预设最小子问题的解
7    dp[1] = 1;
8    dp[2] = 2;
9    // 状态转移:从较小子问题逐步求解较大子问题
10    for (let i = 3; i <= n; i++) {
11        dp[i] = dp[i - 1] + dp[i - 2];
12    }
13    return dp[n];
14}
15

空间优化

如果继续“吹毛求疵”,会发现,我们要求解的是dp[i],而dp[i] 只与 dp[i-1] 和 dp[i-2] 有关,上面代码返回的却是dp[n]?

完全没必要,所以它还有改进空间,便是去除数组,采用两个变量滚动前进。

1/* 爬楼梯:空间优化后的动态规划 */
2function climbingStairsDPComp(n) {
3    if (n === 1 || n === 2) return n;
4    let a = 1,
5        b = 2;
6    for (let i = 3; i <= n; i++) {
7        const tmp = b;
8        b = a + b;
9        a = tmp;
10    }
11    return b;
12}
13

由于省去了数组占用的空间,空间复杂度从 O(n) 降至 O(1) ,再次大幅优化。

我们可以触类旁通地认为,在动态规划问题中,当前状态往往仅与前面有限个状态有关,可以只保留必要的状态,通过“降维”来节省内存空间。

这种空间优化技巧被称为 “滚动变量”“滚动数组”

子问题玄机

本篇文章,我们再次提到“子问题”,“子问题”分解是一种通用的算法思路,之前你肯定就见过,那么,它们之间的区别是什么?

  • 分治算法:强调子问题相互独立。
  • 动态规划:子问题是相互依赖的,在分解过程中会出现许多重叠子问题。
  • 回溯算法:原问题的解由一系列决策步骤构成,每个决策步骤之前的子序列可看作一个子问题。

识别动态规划

如何判断一个问题是不是动态规划?

总的来说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型。

在此基础上,还有一些判断的“加分项”。

  • 问题包含最大(小)或最多(少)等最优化描述。
  • 问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。

小结

本篇文章系统讲解了动态规划的特点和核心实现,除了上面介绍的“爬楼梯”,还有什么常见适用场景吗?

0-1 背包:求解“在限定背包容量下能放入物品的最大价值”,满足决策树模型,含有“最大”关键词。

编辑距离:求解两个字符串之间互相转换的最少修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。当下最火的大语言模型中,就广泛存在此类应用。

动态规划还有很多应用场景,且不易掌握和解决,需要大家正确理解和多练习才行,一起加油!~

更多好文章第一时间推送,可关注公众号:前端说书匠


Hello 算法:“走一步看一步”的智慧》 是转载文章,点击查看原文


相关推荐


Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互
4. 嵌入式铲屎官2026/4/17

Linux 驱动开发入门:从最简单的 hello 驱动到硬件交互 🎉 写给未来的自己和领导:本文是 Linux 驱动开发的 入门级保姆教程,从零开始搭建驱动框架,逐行解释代码,记录每一个踩过的坑。无论你是刚接触内核编程,还是想快速上手 GPIO 中断,都能在这里找到清晰的思路和可复现的步骤。 📚 目录 引言:驱动是什么?驱动的基本框架 —— 一切皆文件实战:第一个 hello 驱动 3.1 完整的驱动源码(带详细注释)3.2 编译驱动 —— Makefile 解析3.3 上机测试 ——


深入剖析 Redis 经典面试题
Thomas.Sir2026/4/9

1、什么是Redis?它主要用来什么的? Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。 与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、


260331-OpenWebUI统计所有Chat的对话字符个数
GuokLiu2026/4/1

1 OWUI启动脚本 # Open-WebUI Settings export DATA_DIR='data0331' export ENABLE_SIGNUP=True export DEFAULT_USER_ROLE='admin' export DEFAULT_GROUP_ID='xai' export OFFLINE_MODE=false export HF_HUB_OFFLINE=1 # OpenAI API 配置 export ENABLE_OLLAMA_API=false ex


[LangChain智能体本质论]中间件是如何参与Agent、Model和Tool三者交互的?
JaydenAI2026/3/23

LangChain的中间件(Middleware)是围绕Agent执行流程构建的“可插拔钩子系统”。它允许开发者在不修改核心逻辑的情况下,在执行的关键节点(如输入处理、模型调用前后、输出解析等)对数据流进行拦截、修改或验证。中间件类型以AgentMiddleware为基类。 1. AgentMiddleware AgentMiddleware是一个泛型类型,两个泛型参数分别代表状态和静态上下文的类型,我们可以利用state_schema字段得到状态类型。它的name属性返回中间件的名称,默认返回


haproxy案例项目(haproxy+dns+nginx+nfs+keepalived)
爱莉希雅&&&2026/3/15

HAProxy+Nginx+NFS+DNS 部署笔记 一、环境规划 主机名IP 地址安装软件角色说明haproxy192.168.72.100/24haproxy负载均衡器nginx1192.168.72.10/24nginx、nfs-utilsWeb 节点 1(挂载 NFS 共享)nginx2192.168.72.20/24nginx、nfs-utilsWeb 节点 2(挂载 NFS 共享)nfs192.168.72.30/24nfs-utilsNFS 文件共享服务器dns192.168.


Spring Cloud+AI :实现分布式智能推荐系统
我不是呆头2026/3/7

欢迎文末添加好友交流,共同进步! “ 俺はモンキー・D・ルフィ。海贼王になる男だ!” 引言 在当今数字化时代,推荐系统已成为电商平台、内容分发平台、社交网络等互联网产品的核心竞争力之一。从淘宝的"猜你喜欢"、抖音的精准内容推送,到 Netflix 的影视推荐,优秀的推荐系统不仅能显著提升用户留存率和转化率,更能为企业带来可观的商业价值。据统计,亚马逊约 35% 的销售额来自推荐系统,Netflix 则通过推荐算法为用户节省了每年约 10 亿美元的搜索成本。 然而,随着业


一文搞懂激活函数!
aicoting2026/2/27

推荐直接网站在线阅读:aicoting.cn 在深度学习中,激活函数(Activation Function)是神经网络的灵魂。它不仅赋予网络非线性能力,还决定了训练的稳定性和模型性能。那么,激活函数到底是什么?为什么我们非用不可?有哪些经典函数?又该如何选择? 所有相关源码示例、流程图、模型配置与知识库构建技巧,我也将持续更新在Github:AIHub,欢迎关注收藏! 1. 什么是激活函数,为什么需要激活函数 激活函数的核心作用就是为神经网络引入非线性。 为什么需要非线性? 想象一下,如果


【Python练习五】Python 正则与网络爬虫实战:专项练习(2道经典练习带你巩固基础——看完包会)
纯.Pure_Jin(g)2026/2/18

第一题 题目: 使用正则完成下列内容的匹配 匹配陕西省区号 029-12345匹配邮政编码 745100匹配邮箱 lijian@xianoupeng.com匹配身份证号 62282519960504337X 代码: import re # 1. 匹配陕西省区号 029-12345 pattern_area = r'^029-\d{5}$' # 精确匹配 029- 开头,后接5位数字 test_area = '029-12345' print("区号匹配:", re.match(pattern_


Claude Code Agent Teams:3个AI同时写代码,底层原理和主流框架对比
易安说AI2026/2/10

大家好,我是易安,AI超级个体,大厂程序员二孩奶爸。 Claude Opus 4.6 带来了 Agent Teams 功能,可以让多个 Claude Code 实例并行工作。我用它做了个小项目,踩了一些坑,也顺便把底层原理和市面上几个主流多 Agent 框架做了个对比。这篇文章干货比较多,建议收藏。 Agent Teams 到底是什么 简单说就是一个 Lead Agent 可以 spawn 出多个 Teammate Agent,每个 Teammate 是一个完全独立的 Claude Code 会


abigen使用教程 - go版本
Warson_L2026/2/1

在 Web3 后端开发中,abigen 是一个至关重要的工具。它能根据 Solidity 合约生成的 ABI(应用二进制接口)自动生成 Go 语言代码,让你像调用普通 Go 函数一样调用智能合约。 以下是详细的 abigen 使用教程。 第一步:安装 abigen 工具 abigen 是 go-ethereum 项目的一部分。你可以通过以下命令安装: # 安装最新版 abigen go install github.com/ethereum/go-ethereum/cmd/abigen@lat

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客