GitHub_Trending/leetcode1/leetcode面试必备:动态规划高频题全解析与实战
【免费下载链接】leetcode Leetcode solutions 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
动态规划面试痛点与解决方案
你是否在面试中遇到动态规划问题时束手无策?是否面对复杂的状态转移方程感到困惑?本文将系统解析动态规划(Dynamic Programming, DP)的核心思想与解题框架,通过8道高频面试题的实战分析,帮你彻底掌握这一面试必备技能。读完本文,你将能够:
- 快速识别动态规划问题特征
- 熟练运用"状态定义-转移方程-边界条件"三步法建模
- 掌握空间优化技巧降低复杂度
- 应对90%以上的DP面试题
动态规划核心原理
动态规划本质
动态规划是一种通过将复杂问题分解为重叠子问题,并存储子问题解来避免重复计算的优化技术。其核心特征包括:
- 重叠子问题:问题可以分解为重复出现的子问题
- 最优子结构:问题的最优解包含子问题的最优解
- 无后效性:当前状态只与过去状态有关,与未来状态无关
解题框架
线性DP专题
爬楼梯问题(LeetCode #70)
问题描述:有n级台阶,每次可以爬1或2级,求有多少种不同的登顶方法。
状态定义:dp[i]表示爬到第i级台阶的方法数
转移方程:dp[i] = dp[i-1] + dp[i-2]
边界条件:dp[1] = 1, dp[2] = 2
代码实现:
int climbStairs(int n) {
if (n <= 3) return n;
int n1 = 2, n2 = 3;
for (int i = 4; i <= n; i++) {
int temp = n1 + n2;
n1 = n2;
n2 = temp;
}
return n2;
}
空间优化:由于只依赖前两个状态,可将O(n)空间优化为O(1)
打家劫舍(LeetCode #198)
问题描述:沿街房屋排列,不能抢劫相邻房屋,求最大抢劫金额。
状态定义:dp[i]表示前i间房屋的最大抢劫金额
转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
边界条件:dp[0] = nums[0], dp[1] = max(nums[0], nums[1])
代码实现:
int rob(int* nums, int numsSize) {
int dp[105];
memset(dp, -1, sizeof(dp));
function<int(int)> solve = [&](int n) {
if (n == 0) return nums[0];
if (n < 0) return 0;
if (dp[n] != -1) return dp[n];
int way1 = nums[n] + solve(n-2); // 抢劫当前房屋
int way2 = solve(n-1); // 不抢劫当前房屋
return dp[n] = max(way1, way2);
};
return solve(numsSize - 1);
}
二维DP专题
不同路径(LeetCode #62)
问题描述:m×n网格,从左上角到右下角,只能向右或向下移动,求不同路径数。
状态定义:dp[i][j]表示到达(i,j)的路径数
转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件:第一行和第一列全为1
代码实现:
int uniquePaths(int m, int n) {
int dp[105][105];
memset(dp, -1, sizeof(dp));
function<int(int, int)> ways = [&](int posX, int posY) {
if (posX == m-1 && posY == n-1) return 1;
if (posX >= m || posY >= n) return 0;
if (dp[posX][posY] != -1) return dp[posX][posY];
int right = ways(posX+1, posY); // 向右移动
int bottom = ways(posX, posY+1); // 向下移动
return dp[posX][posY] = right + bottom;
};
return ways(0, 0);
}
空间优化:使用一维数组dp[j] = dp[j] + dp[j-1]将空间复杂度从O(mn)降至O(n)
编辑距离(LeetCode #72)
问题描述:计算将word1转换为word2所需的最少操作数,允许插入、删除、替换操作。
状态定义:dp[i][j]表示word1前i个字符转换为word2前j个字符的最少操作数
转移方程:
- 若字符相同:
dp[i][j] = dp[i-1][j-1] - 若字符不同:
dp[i][j] = 1 + min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j])
代码实现:
int minDistance(char *word1, char *word2) {
int m = strlen(word1), n = strlen(word2);
int dp[m+1][n+1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0) dp[i][j] = j; // 插入j个字符
else if (j == 0) dp[i][j] = i; // 删除i个字符
else if (word1[i-1] == word2[j-1])
dp[i][j] = dp[i-1][j-1]; // 字符相同无需操作
else
dp[i][j] = 1 + min3(dp[i-1][j-1], dp[i][j-1], dp[i-1][j]);
}
}
return dp[m][n];
}
子序列与子数组专题
最长递增子序列(LeetCode #300)
问题描述:找到数组中最长严格递增子序列的长度。
状态定义:dp[i]表示以nums[i]结尾的最长递增子序列长度
转移方程:dp[i] = max(dp[j] + 1) for j < i and nums[j] < nums[i]
边界条件:dp[i] = 1(每个元素自身是长度为1的子序列)
代码实现:
int lengthOfLIS(int* nums, int numsSize) {
int maxLen = 0;
int* dp = calloc(numsSize, sizeof(int));
dp[0] = nums[0];
for (int k = 0; k < numsSize; k++) {
int i = 0, j = maxLen;
while (i != j) {
int mid = (i + j) / 2;
if (dp[mid] < nums[k]) i = mid + 1;
else j = mid;
}
dp[i] = nums[k];
if (i == maxLen) maxLen++;
}
free(dp);
return maxLen;
}
算法优化:使用二分查找将时间复杂度从O(n²)降至O(nlogn)
最大子数组和(LeetCode #53)
问题描述:找到具有最大和的连续子数组( Kadane算法)。
状态定义:dp[i]表示以nums[i]结尾的最大子数组和
转移方程:dp[i] = max(nums[i], dp[i-1] + nums[i])
边界条件:dp[0] = nums[0]
代码实现:
int maxSubArray(int* nums, int numsSize) {
int result = nums[0], total = 0;
for (int i = 0; i < numsSize; i++) {
total += nums[i];
result = max(result, total);
if (total < 0) total = 0; // 重置当前子数组和
}
return result;
}
空间优化:使用变量滚动存储,空间复杂度从O(n)降至O(1)
背包问题专题
零钱兑换(LeetCode #322)
问题描述:给定不同面额的硬币和总金额,计算凑成总金额所需的最少硬币数。
状态定义:dp[i]表示凑成金额i所需的最少硬币数
转移方程:dp[i] = min(dp[i-coins[j]] + 1) for j in 0..coinsSize-1
边界条件:dp[0] = 0,其他初始化为无穷大
代码实现:
int coinChange(int* coins, int coinsSize, int amount) {
if (amount == 0) return 0;
int dp[amount + 1];
for (int i = 1; i <= amount; i++) dp[i] = UINT_MAX;
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
unsigned int cpt = UINT_MAX;
for (int j = 0; j < coinsSize; j++) {
if (i >= coins[j] && cpt > dp[i - coins[j]]) {
cpt = dp[i - coins[j]];
}
}
if (cpt != UINT_MAX) dp[i] = cpt + 1;
}
return dp[amount] == UINT_MAX ? -1 : dp[amount];
}
分割等和子集(LeetCode #416)
问题描述:判断数组是否可以分割成两个子集,使两个子集的和相等(0-1背包问题)。
状态定义:dp[i]表示是否能组成和为i的子集
转移方程:dp[i] = dp[i] || dp[i-nums[j]] for j in 0..numsSize-1
边界条件:dp[0] = true
代码实现:
bool canPartition(int* nums, int numsSize) {
int totalSum = 0;
for (int i = 0; i < numsSize; i++) totalSum += nums[i];
if (totalSum % 2 != 0) return false; // 和为奇数无法分割
int targetSum = totalSum / 2;
bool dp[targetSum + 1];
memset(dp, false, sizeof(dp));
dp[0] = true;
for (int i = 0; i < numsSize; i++) {
for (int j = targetSum; j >= nums[i]; j--) {
dp[j] = dp[j] || dp[j - nums[i]];
}
}
return dp[targetSum];
}
三角形最小路径和(LeetCode #120)
问题描述:给定三角形,找出自顶向下的最小路径和,每次只能移动到下一行相邻的节点。
状态定义:dp[i][j]表示到达第i行第j列的最小路径和
转移方程:dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
边界条件:第一行dp[0][0] = triangle[0][0]
代码实现:
int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {
// 从 bottom-up 方式计算,优化空间复杂度
for (int i = triangleSize - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]);
}
}
return triangle[0][0];
}
空间优化:直接修改原数组,空间复杂度从O(n²)降至O(1)
动态规划解题策略总结
问题类型与对应策略
| 问题类型 | 核心特征 | 解题策略 | 代表题目 |
|---|---|---|---|
| 线性DP | 一维状态转移 | 单数组滚动存储 | 爬楼梯、打家劫舍 |
| 二维DP | 二维状态矩阵 | 状态压缩优化 | 不同路径、编辑距离 |
| 子序列问题 | 元素不连续 | 二分查找优化 | 最长递增子序列 |
| 背包问题 | 选或不选的决策 | 逆序遍历避免重复 | 零钱兑换、分割等和子集和 |
| 区间DP | 区间状态定义 | 区间长度递增 | 最长回文子序列 |
状态设计三原则
- 无后效性:状态只与过去有关,不影响未来决策
- 最优子结构:问题最优解包含子问题最优解
- 可计算性:能够通过已知状态推导出未知状态
优化技巧
- 空间优化:使用滚动数组、状态压缩将高维转为低维
- 时间优化:利用二分查找、前缀和等技巧减少计算量
- 状态简化:合并重复状态,减少状态维度
实战训练与面试准备
高频面试题训练路径
面试解题四步法
- 问题分析:判断是否为动态规划问题,识别子问题特征
- 状态建模:定义清晰的状态表示,确保无后效性
- 方程推导:根据状态关系推导出转移方程
- 优化实现:考虑时间空间优化,编写高效代码
总结与展望
动态规划作为面试中的重难点,掌握其核心思想和解题框架至关重要。本文通过8道高频面试题的系统解析,从线性DP到二维DP,从子序列问题到背包问题,全面覆盖了动态规划的主要应用场景。记住,动态规划的本质是用空间换时间,通过存储中间结果避免重复计算。
在面试中遇到动态规划问题时,不要急于写代码,先按照"问题分析-状态定义-转移方程-边界条件"的步骤进行思考,确保思路清晰后再动手实现。通过本文提供的训练路径进行系统练习,你一定能够在面试中从容应对各类动态规划问题。
点赞+收藏+关注,获取更多算法面试干货!下期预告:《回溯算法深度解析:从排列组合到子集问题》
祝大家面试顺利,offer拿到手软!
【免费下载链接】leetcode Leetcode solutions 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



