动态规划入门算法
本文针对前端面试高频的动态规划(DP)入门题,拆解 5 道经典题的解题思路,从「问题分析→原理推导→代码实现→答题套路」全流程讲解,帮你吃透动态规划的核心逻辑,面试遇到同类题直接秒杀。
一、动态规划是什么?(面试先懂核心)
动态规划(Dynamic Programming,简称 DP)是一种将复杂问题拆解为子问题的算法思想:
- 核心:找到「状态转移方程」(子问题之间的推导关系)+ 「边界条件」(最小子问题的解)。
- 优势:避免暴力递归的重复计算,将时间复杂度从指数级(O(2ⁿ))优化为线性级(O(n))。
- 前端面试特点:考的都是入门级 DP 题(无复杂状态),核心是「找规律 + 空间优化」。
本文覆盖 5 道前端面试高频 DP 题:爬楼梯、三步问题(爬楼梯扩展)、打家劫舍、最小花费爬楼梯、连续数列(最大子数组和),全部采用「空间优化版」实现(仅用变量存储状态,而非数组)。
🔥 题 1:爬楼梯
1. 问题描述
需要爬 n 阶台阶到楼顶,每次只能爬 1 或 2 阶,求有多少种不同的方法?
2. 算法原理
-
子问题拆解:要到第
i阶,最后一步只能是「从 i-1 阶爬 1 步」或「从 i-2 阶爬 2 步」。 -
状态转移方程:
dp[i] = dp[i-1] + dp[i-2](第 i 阶方法数 = 前两阶方法数之和)。 -
边界条件:
- n=1 → 1 种(只能爬 1 步);
- n=2 → 2 种(1+1 / 2)。
//爬楼梯
function climbStairs(n){
if(n<=0) return 0;
if(n===1) return 1;
if(n===2) return 2;
let a=1,b=2,c;
for(let i=3;i<=n;i++){
// 爬到第i层的方法数等于爬到第(i-1)层和第(i-2)层的方法数之和
c=a+b;
a=b;
b=c;
}
return c;
}
🔥 题 2:三步问题(爬楼梯扩展)
1. 问题描述
每次可以爬 1、2、3 阶,求爬 n 阶的方法数。
- 示例:n=3 → 4 种(1+1+1 / 1+2 / 2+1 / 3)。
2. 算法原理
-
状态转移方程:
dp[i] = dp[i-1] + dp[i-2] + dp[i-3](当前阶 = 前 3 阶方法数之和)。 -
边界条件:
- n=1→1,n=2→2,n=3→4。
//三步问题
function waysToStep(n){
if(n<=0) return 0;
if(n===1) return 1;
if(n===2) return 2;
if(n===3) return 4;
let a=1,b=2,c=4,d;
for(let i=4;i<=n;i++){
// 爬到第i层的方法数等于爬到第(i-1)、(i-2)和(i-3)层的方法数之和
d=a+b+c;
a=b;
b=c;
c=d;
}
return d;
}
🔥 题 3:打家劫舍(状态选择类 DP)
1. 问题描述
沿街有一排房子,每个房子有一定现金,不能抢劫相邻的房子,求能抢劫的最大金额。
- 示例:nums = [1,2,3,1] → 最大金额 4(抢 1+3)。
2. 算法原理
-
子问题拆解:对于第
i个房子,有两种选择:- 抢:总金额 = 前 i-2 个房子的最大金额 + 当前房子金额;
- 不抢:总金额 = 前 i-1 个房子的最大金额。
-
状态转移方程:
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i])。 -
边界条件:
- 空数组 → 0;
- 只有 1 个房子 → 直接抢(nums [0])。
//打家劫舍
function rob(nums){
if(nums.length===0) return 0;
if(nums.length===1) return nums[0];
let prev=0, curr=0;
for(let num of nums){
let temp=curr;
// 当前最大金额等于之前的最大金额和当前房子金额之和,或者之前的最大金额(不抢当前房子)
curr=Math.max(prev+num, curr);
prev=temp; // 更新prev为之前的curr
}
return curr;
}
🔥 题 4:使用最小花费爬楼梯
1. 问题描述
爬楼梯需要支付每阶的费用,每次可以爬 1 或 2 阶,求爬到楼顶的最小花费(楼顶在最后一阶之后)。
- 示例:cost = [10,15,20] → 最小花费 15(爬 1→2 阶,总 10+15?不,最优是爬 2→楼顶,花费 15)。
2. 算法原理
-
子问题拆解:到第
i阶的最小花费 = min (到 i-1 阶花费,到 i-2 阶花费) + 当前阶费用。 -
状态转移方程:
dp[i] = Math.min(dp[i-1], dp[i-2]) + cost[i]。 -
边界条件:
- 第 0 阶花费 = cost [0];
- 第 1 阶花费 = cost [1]。
-
最终结果:楼顶在最后一阶之后,所以取「最后一阶」和「倒数第二阶」的最小值。
//使用最小花费爬楼梯
function minCostClimbingStairs(cost){
let n=cost.length;
if(n===0) return 0;
if(n===1) return cost[0];
let prev=cost[0], curr=cost[1];
for(let i=2;i<n;i++){
let temp=curr;
// 爬到第i层的最小花费等于爬到第(i-1)层和第(i-2)层的最小花费加上当前层的花费
curr=Math.min(prev, curr)+cost[i];
prev=temp; // 更新prev为之前的curr
}
// 最后可以选择爬到最后一层或者直接跳过最后一层,所以返回两者的最小值
return Math.min(prev, curr);
}
🔥 题 5:连续数列(最大子数组和,Kadane 算法)
1. 问题描述
给定整数数组,找一个连续子数组(至少含一个元素),使其和最大,返回该和。
- 示例:nums = [-2,1,-3,4,-1,2,1,-5,4] → 最大和 6(4+-1+2+1)。
2. 算法原理
- 子问题拆解:以第
i个元素结尾的最大子数组和 = max (当前元素本身,前 i-1 个元素的最大和 + 当前元素)。 - 状态转移方程:
currentSum = Math.max(nums[i], currentSum + nums[i])。 - 边界条件:初始最大和 = 第一个元素。
- 核心:用两个变量分别记录「当前子数组和」和「全局最大和」。
//连续数列
function maxSubArray(nums){
if(nums.length===0) return 0;
let maxSum=nums[0];
let currentSum=nums[0];
for(let i=1;i<nums.length;i++){
// 当前连续子数组的和要么是当前元素本身,要么是之前的和加上当前元素
currentSum=Math.max(nums[i], currentSum+nums[i]);
// 更新最大和
maxSum=Math.max(maxSum, currentSum);
}
return maxSum;
}
二、动态规划入门题答题套路
第一步:分析问题,确定「状态」
- 问「方法数」→ 状态是「第 i 个位置的方法数」;
- 问「最大 / 最小值」→ 状态是「第 i 个位置的最优值」;
- 问「是否可行」→ 状态是「第 i 个位置是否可达」。
第二步:找「状态转移方程」
核心是回答:当前状态如何由前面的状态推导出来?
- 爬楼梯:当前 = 前 1 + 前 2;
- 打家劫舍:当前 = max (前 1, 前 2 + 当前值);
- 最大子数组和:当前 = max (当前值,前 1 + 当前值)。
第三步:确定「边界条件」
- 最小子问题的解(如 n=1、n=2 时的结果);
- 空输入、单元素输入的处理。
第四步:优化空间
- 若状态只依赖前 1/2/3 个状态 → 用变量代替数组,空间从 O (n)→O (1);
- 若状态依赖更多 → 用数组存储(如二维 DP)。
第五步:代码实现(规范 + 注释)
- 先处理边界条件(空、特殊值);
- 用变量 / 数组存储状态;
- 循环推导状态,返回最终结果;
- 关键逻辑加注释
三、总结
前端面试中的动态规划题,90% 都是「线性 DP」(状态只依赖前几个),核心是:
- 找规律:通过示例推导状态转移方程;
- 定边界:处理最小子问题;
- 优空间:能用变量就不用数组,面试加分;
- 鲁棒性:处理空输入、特殊值等边界情况。