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

用 “走楼梯” 讲透动态规划!4 个前端场景 + 4 道 LeetCode 题手把手教

动态规划:

如果说算法是解决问题的“套路”,那动态规划就是一种“拆解问题、巧用经验”的套路。它的核心思路可以用一句话概括:把复杂问题拆成小问题,记住小问题的答案,避免重复计算。

先看一个生活例子:算楼梯台阶

假设你要上10级台阶,每次只能走1级或2级,一共有多少种走法?

直接算10级很难,不如拆成小问题:

  • 上10级的最后一步,要么是从第9级走1级,要么是从第8级走2级。
  • 所以,10级的走法 = 9级的走法 + 8级的走法。
  • 同理,9级的走法 = 8级的走法 + 7级的走法……
  • 直到最基础的情况:1级只有1种走法,2级有两种走法(1+1或2)。

这里的关键是:后面的答案依赖前面的答案,而前面的答案可以重复利用。 我们不用每次都重新算,只要把算过的结果记下来(比如用一个数组存起来),就能一步步推导出最终答案。这就是动态规划的核心:用子问题的解推导父问题的解,并用“备忘录”保存子问题答案。

动态规划的3个核心要素

  1. 状态定义:

用一个“变量”描述子问题的核心。比如上面的例子中,dp[n]代表"上n级台阶的走法数",这个dp[n]就是“状态”。

  1. 状态转移方程:

子问题之间的关系。比如dp[n] = dp[n-1] + dp[n-2],就是用n-1和n-2的解推导n的解。

  1. 初始条件:

最基础的子问题答案。比如dp[1] = 1, dp[2] = 2,没有这些“起点”,就无法推导后面的结果。

为什么要用动态规划?

最大的好处是减少重复计算。

比如算10级台阶时,如果不用动态规划,可能会递归计算dp[9]和dp[8],而算dp[9]时又会重复算dp[8]和dp[7]……越往后重复越多,效率越低。

动态规划通过“记录子问题答案”,让每个子问题只算一次,把时间复杂度从指数级降到线性级(比如上面的例子,从O(2ⁿ)降到O(n))。

前端开发中的常见场景

  1. 路径规划:

比如网格类组件(如地图、表格)中,计算从左上角到右下角的最短路径(每次只能右移或下移),可以用动态规划拆解为“当前格子的最短路径=左边格子的路径 + 上边格子的路径”。

  1. 最长公共子序列:

比较两个字符串的相似性(如文本diff功能),比如“abcde”和“ace”的最长公共子序列是“ace”,可以用动态规划一步步对比字符。

  1. 资源分配:

比如前端性能优化中,给多个任务分配有限的时间/内存,求最大收益(类似“背包问题”),用动态规划决定每个任务分配多少资源。

  1. 状态记忆:

比如在表单中,记录用户每一步的输入状态,回退时直接复用之前的结果,避免重新计算(本质是用“备忘录”保存子状态)。

以leetcode第64题为例演示路径规划

代码如下:

/*** @param {number[][]} grid* @return {number}*/
var minPathSum = function(grid) {if(grid.length === 0 || grid[0].length === 0) {return 0;}const rows = grid.length;const cols = grid[0].length;const dp = new Array(rows);for(let i = 0; i < rows; i++) {dp[i] = new Array(cols).fill(0)}// 初始条件 dp[0][0] = grid[0][0];// 第一行for(let j = 1; j < cols; j++) {dp[0][j] = dp[0][j-1] + grid[0][j]}// 第一列for(let i = 1; i < rows; i++) {dp[i][0] = dp[i-1][0] + grid[i][0]}for(let i = 1; i < rows; i++) {for(let j = 1; j < cols; j++) {dp[i][j] = grid[i][j] + Math.min(dp[i-1][j], dp[i][j-1])}}return dp[rows-1][cols-1];
};// 测试用例
const grid1 = [[1, 3, 1],[1, 5, 1],[4, 2, 1]
];
console.log("网格1的最短路径和:", minPathSum(grid1)); // 输出:7,路径是 1→3→1→1→1const grid2 = [[1, 2, 3],[4, 5, 6]
];
console.log("网格2的最短路径和:", minPathSum(grid2)); // 输出:12,路径是 1→2→3→6

以leetcode第1143题为例演示最长公共子序列

代码如下:

var longestCommonSubsequence = function(text1, text2) {const m = text1.length;const n = text2.length;const dp = new Array(m+1).fill(0).map(() => new Array(n+1).fill(0))for(let i = 1; i <= m; i++) {for(let j = 1; j <= n; j++) {if(text1[i-1] === text2[j-1]) {dp[i][j] = dp[i-1][j-1] + 1;} else {dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])}}}return dp[m][n];
};// 测试用例
console.log(longestCommonSubsequence("abcde", "ace")); // 输出:3(公共子序列为"ace")
console.log(longestCommonSubsequence("abc", "abc"));   // 输出:3(公共子序列为"abc")
console.log(longestCommonSubsequence("abc", "def"));   // 输出:0(无公共子序列)

以leetcode第455题(分发饼干)为例演示贪心分配

代码如下:

var findContentChildren = function(g, s) {// 对胃口和饼干尺寸排序g.sort((a, b) => a - b);s.sort((a, b) => a - b);let i = 0; // 孩子指针let j = 0; // 饼干指针let count = 0; // 满足孩子的数量while(i < g.length && j < s.length) {if(s[j] >= g[i]) {count++;i++;j++;} else {// 饼干小,尝试更大尺寸的饼干j++;}} return count;
};// 测试用例
console.log(findContentChildren([1,2,3], [1,1])); // 1
console.log(findContentChildren([1,2], [1,2,3])); // 2
console.log(findContentChildren([2,3,4], [1,3,5])); // 2(3满足2,5满足3)

以leetcode第509题(经典的斐波那契数列)为例演示状态记忆

var fib = function(n) {const memo = new Array(n + 1).fill(-1);const dfs = (k) => {if(k === 0) return 0;if(k === 1) return 1;if(memo[k] !== -1) return memo[k];memo[k] = dfs(k - 1) + dfs(k - 2);return memo[k]}return dfs(n)
};// 测试用例
console.log(fib(2)); // 1
console.log(fib(3)); // 2
console.log(fib(4)); // 3

总结:

动态规划就像“走楼梯时,记住每一级台阶有几种走法,不用每次都从头数”。它通过拆解问题、记录中间结果,让复杂问题变得简单高效,是前端处理“有依赖关系的多步骤问题”的利器。


http://www.xdnf.cn/news/19739.html

相关文章:

  • 戴尔笔记本电池健康度检测、无电池开机测试与更换电池全流程记录
  • 孩子玩手机都近视了,怎样限制小孩的手机使用时长?
  • 你只需输入一句话,MoneyPrinterTurbo直接给你输出一个视频
  • 小说、漫剧小程序系统开发:独立部署,源码交付
  • SpringBoot Web 入门指南:从零搭建第一个SpringBoot程序
  • 【leetcode】200. 岛屿数量
  • 有限元方法中的数值技术:预处理共轭梯度法 PCG (2)
  • 【Cursor-Gpt-5-high】StackCube-v1 任务训练结果不稳定性的分析
  • 关于linux网络编程——4
  • 醋酸铕:点亮现代生活的“隐形之光“
  • HTML元素周期表
  • 【C++】C++入门—(中)
  • ASP.NET Web Forms 实战:用 RadioButton 打造“性别/称谓选择”表单的最佳实践
  • 【数据结构】1绪论
  • 【Qt中信号槽连接connect有接收者和无接收者的区别】
  • 执行一条select语句期间发生了什么?
  • 常用符号 Emoji 对照表——Unicode UTF-8
  • CSS Sass Less 样式.xxx讲解
  • SpringMVC的请求接收与结果响应
  • 华为HCIE数通含金量所剩无几?考试难度加大?
  • 数据库选择有讲究?SQLite、PostgreSQL还是MySQL?
  • 电脑接入企业中的网线,为啥网卡上面显示AD域名
  • MongoDB 聚合查询超时:索引优化与分片策略的踩坑记录
  • 国产CAD皇冠CAD(CrownCAD)建模教程:汽车驱动桥
  • 二、Scala流程控制:分支与循环
  • 波浪模型SWAN学习(2)——波浪浅化模拟(Shoaling on sloping beach)
  • RoPE频率缩放机制:解密大语言模型上下文扩展的核心算法
  • linux之IO存储子系统全流程分析
  • 差分隐私在运营指标:ABP 的 DP 计数器与噪声预算
  • 使用PyTorch构建全连接神经网络实现MNIST手写数字分类