GitHub_Trending/leetcode1/leetcode面试必备:动态规划高频题全解析与实战

GitHub_Trending/leetcode1/leetcode面试必备:动态规划高频题全解析与实战

【免费下载链接】leetcode Leetcode solutions 【免费下载链接】leetcode 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

动态规划面试痛点与解决方案

你是否在面试中遇到动态规划问题时束手无策?是否面对复杂的状态转移方程感到困惑?本文将系统解析动态规划(Dynamic Programming, DP)的核心思想与解题框架,通过8道高频面试题的实战分析,帮你彻底掌握这一面试必备技能。读完本文,你将能够:

  • 快速识别动态规划问题特征
  • 熟练运用"状态定义-转移方程-边界条件"三步法建模
  • 掌握空间优化技巧降低复杂度
  • 应对90%以上的DP面试题

动态规划核心原理

动态规划本质

动态规划是一种通过将复杂问题分解为重叠子问题,并存储子问题解来避免重复计算的优化技术。其核心特征包括:

  • 重叠子问题:问题可以分解为重复出现的子问题
  • 最优子结构:问题的最优解包含子问题的最优解
  • 无后效性:当前状态只与过去状态有关,与未来状态无关

解题框架

mermaid

线性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区间状态定义区间长度递增最长回文子序列

状态设计三原则

  1. 无后效性:状态只与过去有关,不影响未来决策
  2. 最优子结构:问题最优解包含子问题最优解
  3. 可计算性:能够通过已知状态推导出未知状态

优化技巧

  • 空间优化:使用滚动数组、状态压缩将高维转为低维
  • 时间优化:利用二分查找、前缀和等技巧减少计算量
  • 状态简化:合并重复状态,减少状态维度

实战训练与面试准备

高频面试题训练路径

mermaid

面试解题四步法

  1. 问题分析:判断是否为动态规划问题,识别子问题特征
  2. 状态建模:定义清晰的状态表示,确保无后效性
  3. 方程推导:根据状态关系推导出转移方程
  4. 优化实现:考虑时间空间优化,编写高效代码

总结与展望

动态规划作为面试中的重难点,掌握其核心思想和解题框架至关重要。本文通过8道高频面试题的系统解析,从线性DP到二维DP,从子序列问题到背包问题,全面覆盖了动态规划的主要应用场景。记住,动态规划的本质是用空间换时间,通过存储中间结果避免重复计算。

在面试中遇到动态规划问题时,不要急于写代码,先按照"问题分析-状态定义-转移方程-边界条件"的步骤进行思考,确保思路清晰后再动手实现。通过本文提供的训练路径进行系统练习,你一定能够在面试中从容应对各类动态规划问题。

点赞+收藏+关注,获取更多算法面试干货!下期预告:《回溯算法深度解析:从排列组合到子集问题》

祝大家面试顺利,offer拿到手软!

【免费下载链接】leetcode Leetcode solutions 【免费下载链接】leetcode 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值