动态规划十题通关:从爬楼梯到编辑距离(Python + C++)

动态规划十题通关:从爬楼梯到编辑距离(Python + C++)

动态规划(DP)是算法面试中最重要也最灵活的专题。核心思想是将大问题分解为重叠子问题,通过状态转移避免重复计算。本文整理了10道经典DP题目,每道题包含:题目描述、状态定义、状态转移方程、图解(表格/文本)、Python代码、C++代码、复杂度分析。掌握这些,DP类题目基本通关。


📌 题目清单

题号 题目 核心考点
70 爬楼梯 DP 入门(斐波那契)
118 杨辉三角 二维 DP
121 买卖股票的最佳时机 一次交易(记录最小价格)
300 最长递增子序列 O(n²) / 二分贪心 O(n log n)
1143 最长公共子序列 二维 DP(经典)
5 最长回文子串 中心扩展 / DP
322 零钱兑换 完全背包(最少硬币数)
416 分割等和子集 0-1 背包(是否存在子集和 = sum/2)
198 打家劫舍 一维 DP(隔房取最大值)
72 编辑距离 二维 DP(三个操作)

1. 爬楼梯(LeetCode 70)

题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。有多少种不同的方法可以爬到楼顶?

示例

输入:n = 3 → 输出:3(1+1+1, 1+2, 2+1)

解题思路

  • 定义 dp[i] 表示到达第 i 阶的方法数。
  • 状态转移:dp[i] = dp[i-1] + dp[i-2](因为可以从 i-1 迈1步,或从 i-2 迈2步)。
  • 初始化:dp[0]=1(地面),dp[1]=1(只有1步)。
  • 实际上就是斐波那契数列。

图解(n=5)

复制代码
i: 0   1   2   3   4   5
dp:1   1   2   3   5   8

Python代码

python 复制代码
def climbStairs(n):
    if n <= 2:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    dp[2] = 2
    for i in range(3, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

# 空间优化版
def climbStairs(n):
    a, b = 1, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

C++代码

cpp 复制代码
class Solution {
public:
    int climbStairs(int n) {
        if (n <= 2) return n;
        int a = 1, b = 2;
        for (int i = 3; i <= n; ++i) {
            int c = a + b;
            a = b;
            b = c;
        }
        return b;
    }
};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)(优化后)

2. 杨辉三角(LeetCode 118)

题目描述

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

示例

输入:numRows = 5

输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

解题思路

  • 使用二维列表 dpdp[i][j] 表示第i行第j个元素。
  • 状态转移:dp[i][j] = dp[i-1][j-1] + dp[i-1][j](当 j 不在首尾时)。
  • 每行首尾均为1。

图解

复制代码
行0: 1
行1: 1 1
行2: 1 (1+1) 1  → 1,2,1
行3: 1 (1+2) (2+1) 1 → 1,3,3,1
...

Python代码

python 复制代码
def generate(numRows):
    res = []
    for i in range(numRows):
        row = [1] * (i + 1)
        for j in range(1, i):
            row[j] = res[i-1][j-1] + res[i-1][j]
        res.append(row)
    return res

C++代码

cpp 复制代码
class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> res;
        for (int i = 0; i < numRows; ++i) {
            vector<int> row(i+1, 1);
            for (int j = 1; j < i; ++j) {
                row[j] = res[i-1][j-1] + res[i-1][j];
            }
            res.push_back(row);
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度:O(numRows²)
  • 空间复杂度:O(numRows²)(输出所需)

3. 买卖股票的最佳时机(LeetCode 121)

题目描述

给定一个数组 prices,其中 prices[i] 是第 i 天的股票价格。最多只能完成一笔交易(买入一次,卖出一次),求最大利润。

示例

输入:[7,1,5,3,6,4] → 输出:5(第2天买入,第5天卖出,利润5)

解题思路

  • 维护历史最低价 minPrice,以及当前最大利润 maxProfit
  • 遍历每一天,更新最低价,并计算以当天卖出能获得的利润,更新最大利润。
  • 等价于 DP:dp[i] = max(dp[i-1], prices[i] - minPrice)

图解

复制代码
prices: 7  1  5  3  6  4
min:    7  1  1  1  1  1
profit: 0  0  4  2  5  3 → max=5

Python代码

python 复制代码
def maxProfit(prices):
    min_price = float('inf')
    max_profit = 0
    for p in prices:
        min_price = min(min_price, p)
        max_profit = max(max_profit, p - min_price)
    return max_profit

C++代码

cpp 复制代码
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int minPrice = INT_MAX, maxProfit = 0;
        for (int p : prices) {
            minPrice = min(minPrice, p);
            maxProfit = max(maxProfit, p - minPrice);
        }
        return maxProfit;
    }
};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

4. 最长递增子序列(LeetCode 300)

题目描述

给定一个整数数组 nums,找到其中最长的严格递增子序列的长度。

示例

输入:[10,9,2,5,3,7,101,18] → 输出:4([2,3,7,101])

解题思路

  • 方法一:DP O(n²)
    dp[i] 表示以 nums[i] 结尾的最长递增子序列长度。
    转移:dp[i] = max(dp[j]) + 1,其中 j < inums[j] < nums[i]
    最终答案 max(dp)
  • 方法二:贪心 + 二分 O(n log n)
    维护一个数组 tailstails[i] 表示长度为 i+1 的递增子序列的最小末尾值。
    遍历每个数,用二分找到第一个大于等于它的位置替换,若找不到则追加。

图解(DP)

复制代码
nums: 10  9  2  5  3  7  101  18
dp:    1  1  1  2  2  3   4   4
max=4

Python代码(DP O(n²))

python 复制代码
def lengthOfLIS(nums):
    n = len(nums)
    dp = [1] * n
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp) if n else 0

C++代码(二分贪心 O(n log n))

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> tails;
        for (int x : nums) {
            auto it = lower_bound(tails.begin(), tails.end(), x);
            if (it == tails.end()) tails.push_back(x);
            else *it = x;
        }
        return tails.size();
    }
};

复杂度分析

  • 时间复杂度:O(n²)(DP) / O(n log n)(二分贪心)
  • 空间复杂度:O(n)

5. 最长公共子序列(LeetCode 1143)

题目描述

给定两个字符串 text1text2,返回它们的最长公共子序列的长度。

示例

输入:text1 = "abcde", text2 = "ace" → 输出:3("ace")

解题思路

  • 定义 dp[i][j] 表示 text1[0..i-1]text2[0..j-1] 的最长公共子序列长度。
  • 状态转移:
    • 如果 text1[i-1] == text2[j-1]dp[i][j] = dp[i-1][j-1] + 1
    • 否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])
  • 初始化 dp[0][j] = dp[i][0] = 0

图解(表格)

复制代码
    "" a c e
""  0 0 0 0
a   0 1 1 1
b   0 1 1 1
c   0 1 2 2
d   0 1 2 2
e   0 1 2 3

Python代码

python 复制代码
def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    return dp[m][n]

C++代码

cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        for (int i = 1; i <= m; ++i) {
            for (int 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] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
        return dp[m][n];
    }
};

复杂度分析

  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)(可优化为 O(min(m,n)))

6. 最长回文子串(LeetCode 5)

题目描述

给定一个字符串 s,找到 s 中最长的回文子串。

示例

输入:"babad" → 输出:"bab""aba"

解题思路

  • 方法一:中心扩展 (推荐,O(n²))
    枚举每个中心(单字符或双字符),向两边扩展找到最大回文。
  • 方法二:DP
    定义 dp[i][j] 表示 s[i..j] 是否为回文。
    转移:dp[i][j] = (s[i]==s[j]) and (j-i<3 or dp[i+1][j-1])

图解(中心扩展)

复制代码
s = "babad"
以 i=2 'b' 为中心,扩展得 "bab" 长度3
以 i=1 'a'? 实际中心有单双,最终最长 "bab"

Python代码(中心扩展)

python 复制代码
def longestPalindrome(s):
    if not s:
        return ""
    start, end = 0, 0
    def expand(l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1
        return l+1, r-1
    for i in range(len(s)):
        l1, r1 = expand(i, i)
        l2, r2 = expand(i, i+1)
        if r1 - l1 > end - start:
            start, end = l1, r1
        if r2 - l2 > end - start:
            start, end = l2, r2
    return s[start:end+1]

C++代码(DP)

cpp 复制代码
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2) return s;
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        int start = 0, maxLen = 1;
        for (int i = 0; i < n; ++i) dp[i][i] = true;
        for (int len = 2; len <= n; ++len) {
            for (int i = 0; i + len <= n; ++i) {
                int j = i + len - 1;
                if (s[i] == s[j]) {
                    if (len == 2 || dp[i+1][j-1]) {
                        dp[i][j] = true;
                        if (len > maxLen) {
                            maxLen = len;
                            start = i;
                        }
                    }
                }
            }
        }
        return s.substr(start, maxLen);
    }
};

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)(中心扩展)/ O(n²)(DP)

7. 零钱兑换(LeetCode 322)

题目描述

给定不同面额的硬币 coins 和一个总金额 amount,计算可以凑成总金额所需的最少硬币个数。如果无法凑成,返回 -1。每种硬币无限使用。

示例

输入:coins = [1,2,5], amount = 11 → 输出:3(11 = 5+5+1)

解题思路

  • 完全背包问题(硬币无限)。
  • 定义 dp[i] 表示凑成金额 i 所需的最少硬币数。
  • 初始化 dp[0]=0,其他为 inf
  • 对每个金额 i,遍历硬币 c,如果 i >= c,则 dp[i] = min(dp[i], dp[i-c] + 1)
  • 最终 dp[amount] 若为 inf 则返回 -1。

图解(amount=11, coins=[1,2,5])

复制代码
dp[0]=0
i=1: dp[1]=min(dp[0]+1)=1
i=2: dp[2]=min(dp[1]+1, dp[0]+1)=min(2,1)=1
i=3: dp[3]=min(dp[2]+1, dp[1]+1)=2
i=4: dp[4]=min(dp[3]+1, dp[2]+1)=2
i=5: dp[5]=min(dp[4]+1, dp[3]+1, dp[0]+1)=min(3,3,1)=1
i=6: dp[6]=min(dp[5]+1, dp[4]+1, dp[1]+1)=2
...
i=11: dp[11]=min(dp[10]+1, dp[9]+1, dp[6]+1)=3

Python代码

python 复制代码
def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for i in range(1, amount + 1):
        for c in coins:
            if i >= c:
                dp[i] = min(dp[i], dp[i - c] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

C++代码

cpp 复制代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            for (int c : coins) {
                if (i >= c && dp[i - c] != INT_MAX) {
                    dp[i] = min(dp[i], dp[i - c] + 1);
                }
            }
        }
        return dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};

复杂度分析

  • 时间复杂度:O(amount × len(coins))
  • 空间复杂度:O(amount)

8. 分割等和子集(LeetCode 416)

题目描述

给定一个只包含正整数的非空数组,判断是否可以将数组分割成两个子集,使得两个子集的元素和相等。

示例

输入:[1,5,11,5] → 输出:true([1,5,5]和[11])

输入:[1,2,3,5] → 输出:false

解题思路

  • 先求和,若和为奇数则 false。
  • 目标:是否存在子集和为 target = sum/2
  • 0-1 背包问题:dp[j] 表示能否凑出和为 j
  • 初始化 dp[0] = true,遍历每个数 num,倒序更新 dp[j] = dp[j] or dp[j - num]

图解([1,5,11,5], target=11)

复制代码
初始 dp[0]=true
num=1: dp[1]=true
num=5: dp[5]=true, dp[6]=true (5+1)
num=11: dp[11]=true
num=5: dp[?] 已满足
最终 dp[11]=true

Python代码

python 复制代码
def canPartition(nums):
    total = sum(nums)
    if total % 2 != 0:
        return False
    target = total // 2
    dp = [False] * (target + 1)
    dp[0] = True
    for num in nums:
        for j in range(target, num - 1, -1):
            dp[j] = dp[j] or dp[j - num]
    return dp[target]

C++代码

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if (sum % 2) return false;
        int target = sum / 2;
        vector<bool> dp(target + 1, false);
        dp[0] = true;
        for (int num : nums) {
            for (int j = target; j >= num; --j) {
                dp[j] = dp[j] || dp[j - num];
            }
        }
        return dp[target];
    }
};

复杂度分析

  • 时间复杂度:O(n × target)
  • 空间复杂度:O(target)

9. 打家劫舍(LeetCode 198)

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内藏有一定的现金,但不能偷相邻的房屋,否则会触发警报。计算能偷窃到的最高金额。

示例

输入:[1,2,3,1] → 输出:4(偷1号(1)和3号(3)=4)

解题思路

  • 定义 dp[i] 表示前 i 间房屋能偷到的最高金额(i从0开始)。
  • 转移:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
  • 边界:dp[0]=nums[0]dp[1]=max(nums[0], nums[1])
  • 空间优化:只需两个变量。

图解

复制代码
nums: 1,2,3,1
dp0=1
dp1=max(1,2)=2
i=2: dp2=max(2, 1+3)=4
i=3: dp3=max(4, 2+1)=4

Python代码

python 复制代码
def rob(nums):
    if not nums:
        return 0
    if len(nums) == 1:
        return nums[0]
    dp = [0] * len(nums)
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    for i in range(2, len(nums)):
        dp[i] = max(dp[i-1], dp[i-2] + nums[i])
    return dp[-1]

# 空间优化
def rob(nums):
    prev, curr = 0, 0
    for num in nums:
        prev, curr = curr, max(curr, prev + num)
    return curr

C++代码

cpp 复制代码
class Solution {
public:
    int rob(vector<int>& nums) {
        int prev = 0, curr = 0;
        for (int num : nums) {
            int temp = curr;
            curr = max(curr, prev + num);
            prev = temp;
        }
        return curr;
    }
};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

10. 编辑距离(LeetCode 72)

题目描述

给你两个单词 word1word2,请返回将 word1 转换成 word2 所使用的最少操作数。你可以对一个单词进行三种操作:插入一个字符、删除一个字符、替换一个字符。

示例

输入:word1 = "horse", word2 = "ros" → 输出:3

(horse → rorse(替换h为r)→ rose(删除r)→ ros(删除e))

解题思路

  • 定义 dp[i][j] 表示 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最少操作数。
  • 转移:
    • 如果 word1[i-1] == word2[j-1]dp[i][j] = dp[i-1][j-1]
    • 否则 dp[i][j] = min( dp[i-1][j] (删除), dp[i][j-1] (插入), dp[i-1][j-1] (替换) ) + 1
  • 初始化:dp[i][0] = i(删除i次),dp[0][j] = j(插入j次)。

图解(表格)

复制代码
    ""  r  o  s
""  0  1  2  3
h   1  1  2  3
o   2  2  1  2
r   3  2  2  2
s   4  3  3  2
e   5  4  4  3
右下角 dp[5][3] = 3

Python代码

python 复制代码
def minDistance(word1, word2):
    m, n = len(word1), len(word2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i-1] == word2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
    return dp[m][n]

C++代码

cpp 复制代码
class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size(), n = word2.size();
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
        for (int i = 0; i <= m; ++i) dp[i][0] = i;
        for (int j = 0; j <= n; ++j) dp[0][j] = j;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (word1[i-1] == word2[j-1]) {
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
                }
            }
        }
        return dp[m][n];
    }
};

复杂度分析

  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)(可优化为 O(min(m,n)))

🎯 总结

题目 核心技巧 时间复杂度 空间复杂度
70. 爬楼梯 斐波那契 O(n) O(1)
118. 杨辉三角 二维递推 O(n²) O(n²)
121. 买卖股票 记录最小价格 O(n) O(1)
300. 最长递增子序列 DP / 二分贪心 O(n log n) O(n)
1143. 最长公共子序列 二维DP O(mn) O(mn)
5. 最长回文子串 中心扩展 / DP O(n²) O(1)
322. 零钱兑换 完全背包 O(amount×len) O(amount)
416. 分割等和子集 0-1背包 O(n×target) O(target)
198. 打家劫舍 一维DP(隔房) O(n) O(1)
72. 编辑距离 二维DP(编辑操作) O(mn) O(mn)

动态规划的核心步骤:

  1. 定义状态(dp数组的含义)
  2. 找到状态转移方程
  3. 确定初始化和边界条件
  4. 确定遍历顺序
  5. 优化空间(可选)

多做、多画表、多总结模式(背包、序列、区间、状态机等),DP会变得有迹可循。

相关推荐
ㄟ留恋さ寂寞1 小时前
HTML5中SharedWorker生命周期与浏览器进程关闭的关系
jvm·数据库·python
彳亍1011 小时前
MongoDB备节点无法读取数据怎么解决_rs.slaveOk()与Secondary读取权限
jvm·数据库·python
m0_690825821 小时前
CSS如何实现圆形头像裁剪_使用border-radius50属性
jvm·数据库·python
披着假发的程序唐1 小时前
STM32 H743 MPU的配置使用方法
linux·c语言·c++·驱动开发·stm32·单片机·mcu
栈溢出了1 小时前
GAT(Graph Attention Network)学习笔记
人工智能·深度学习·算法·机器学习
Tutankaaa1 小时前
学校知识竞赛怎么组织?从班级到年级的进阶方案
经验分享·学习·算法·职场和发展
老纪1 小时前
HTML函数工具在NAS设备上能运行吗_轻服务器适配指南【指南】
jvm·数据库·python
老纪1 小时前
SQL如何高效提取大表前几行:分页查询与OFFSET优化
jvm·数据库·python
梦想不只是梦与想1 小时前
python中的运算符
python·运算符