算法-动态规划入门算法题

0 阅读6分钟

动态规划入门算法

本文针对前端面试高频的动态规划(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」(状态只依赖前几个),核心是:

  1. 找规律:通过示例推导状态转移方程;
  2. 定边界:处理最小子问题;
  3. 优空间:能用变量就不用数组,面试加分;
  4. 鲁棒性:处理空输入、特殊值等边界情况。