动态规划DP
此博客参考
动态规划(Dynamic Programming, DP)。
核心思想:
动态规划是一种解决复杂问题的高效算法设计范式。它的核心思想是将问题分解成相互重叠的子问题,并将子问题的解存储起来(记忆化),避免重复计算,从而大幅提升效率。
你可以把它看作一种用空间换时间的优化策略。
为什么需要动态规划?
许多看似复杂的问题(尤其是优化问题,如求最大值、最小值、方案数等)可以被分解成更小的子问题。如果这些子问题不是独立的,而是会被重复计算多次,那么使用普通的递归(自顶向下)会导致指数级的时间复杂度。
1.入门动态规划DP
例题:
1.一维DP
题目描述
今有面值为 1、5、11 元的硬币各无限枚。
想要凑出 n 元,问需要的最少硬币数量。
输入格式
仅一行,一个正整数 n。
输出格式
仅一行,一个正整数,表示需要的硬币个数。
输入输出样例
输入
输出
说明/提示
样例解释
对于样例数据 1,最佳方案是 15=5+5+5,使用到 3 枚硬币。
对于样例数据 2,最佳方案是 12=11+1,使用到 2 枚硬币。
数据规模与约定
对于 100% 的数据,保证 n≤106。
问题分析
这是一个典型的动态规划问题,属于最优化问题。我们需要用最少数量的硬币凑出给定的金额n,硬币面值为1、5、11元且数量无限。
动态规划思路
-
定义状态:
dp[i]表示凑出金额i所需的最少硬币数量
-
状态转移方程:
- 对于每个金额i,考虑使用三种硬币中的一种:
- 如果用1元硬币:
dp[i] = dp[i-1] + 1 - 如果用5元硬币:
dp[i] = dp[i-5] + 1(当i≥5时) - 如果用11元硬币:
dp[i] = dp[i-11] + 1(当i≥11时)
- 如果用1元硬币:
- 取这三种情况的最小值:
dp[i] = min(dp[i-1], dp[i-5], dp[i-11]) + 1
- 对于每个金额i,考虑使用三种硬币中的一种:
-
初始化:
dp[0] = 0(凑出0元需要0个硬币)- 其他初始值可以设为一个大数(如INT_MAX),表示暂时无法凑出
-
计算顺序:
- 从小到大计算,从1计算到n,确保计算dp[i]时所需的子问题都已解决
参考代码:
二维DP
题目描述
有一个机器人的位于一个 m × n 个网格左上角。
机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。
问有多少条不同的路径?
n和m均不超过100
输入格式
n 和 m
输出格式
路径条数
样例
样例输入
样例输出
核心思路
这是一个典型的网格路径计数问题,可以通过动态规划高效解决。机器人只能向右或向下移动,因此每个位置的路径数等于上方和左方位置路径数之和。
关键步骤
-
状态定义
- 使用二维数组
dp[i][j]表示从起点到网格(i,j)的路径数35。
- 使用二维数组
-
状态转移方程
- 对于非边界位置:
dp[i][j] = dp[i-1][j] + dp[i][j-1] - 边界条件(第一行/列):路径数恒为1(只能单向移动)。
- 对于非边界位置:
-
初始化
dp[j] = 1(第一行)dp[i] = 1(第一列)。
参考代码:
2.基本线性DP
例题:
题目描述
给定一个序列,从中选取若干个数,使得这一组数组成的序列a满足 i < j 且 a[i] < a[j],求这个序列的最长长度
输入格式
第一行一个整数n
接下来一行n个整数表示序列
输出格式
一行一个整数表示最长序列的长度
样例
样例输入
样例输出
数据范围与提示
组成的数列为1 2 4
对于30%的数据, n <= 2000
对于100%的数据,n <= 100000
问题理解
题目要求找出给定序列中最长的严格递增子序列的长度。子序列不需要连续,但必须保持原序列中的相对顺序。
动态规划解法(O(n²))
-
状态定义
dp[i]表示以第i个元素结尾的最长递增子序列长度。
-
状态转移
- 对于每个元素,检查前面所有比它小的元素,取最大dp值加1:
dp[i] = max(dp[i], dp[j] + 1) // 对所有j < i且nums[j] < nums[i]
- 对于每个元素,检查前面所有比它小的元素,取最大dp值加1:
-
边界条件
- 初始时每个元素的dp值至少为1(自身构成子序列)。
优化解法(O(n logn))
-
贪心+二分查找
- 维护一个动态数组
tail,tail[i]表示长度为i+1的子序列的最小末尾元素。 - 遍历时用二分查找确定当前元素的插入位置,保持数组有序。
- 维护一个动态数组
-
关键操作
- 若当前元素大于
tail末尾元素,直接追加。 - 否则替换第一个大于等于它的元素。
- 若当前元素大于
思考1:如果要输出最靠前的一条路径怎么办?
思考2:如果是环形的怎么办?
(需要参考代码评论)
3.划分型动态规划
例题:
题目描述
我们说一个字符序列是回文的,如果它正向和反向写都是一样的。例如,“racecar”就是一个回文,但是“fastcar”不是。
一个字符序列的划分是指一个由一个或多个不相交的、非空的、连续字符组成的列表,这些字符组拼接起来可以得到原始序列。例如,(‘race’, ‘car’)是“racecar”的划分成两组的一个例子。
给定一个字符序列,我们总是可以创建这些字符的一个划分,使得划分中的每一组都是回文的!基于这个观察,很自然会问:
给定一个字符串,使得每一组都是回文的,需要的最小组数是多少?
例如:
• “racecar”已经是一个回文,因此它可以被划分成一组。
• “fastcar”不包含任何非平凡的回文,所以它必须被划分为(‘f’, ‘a’, ‘s’, ‘t’, ‘c’, ‘a’, ‘r’)。
• “aaadbccb”可以被划分为(‘aaa’, ‘d’, ‘bccb’)。
输入格式
输入开始于测试案例的数目n。
每个测试案例由一行由1到1000个小写字母组成,中间没有空白。
输出格式
对于每个测试案例,在一行中输出将输入划分成回文组所需要的最小组数。
样例
输入样例
输出样例
思路
状态表示: f[i] 是 1~i 中回文串个数
状态计算: f[i] = min(f[i], f[j] + 1 ) if i~j 是一个回文串
所以先用n^2把这个串中的所有回文字串标识出来。
这里的 st 只是为了确保p数组中的值是当前数据组的情况, 正常的判断回文数也可以给st去掉然后每组数据重置一下p数组就行。建议记下来以后判断回文串除了双指针就用递归整。
代码
4.背包问题
4.1 01背包
题目描述
有一个最多能装m(1<m<=30)千克的背包,有n(1<n<=15)块魔法石,它们的重量分别是W1,W2,...,Wn,它们的价值分别是C1,C2,...,Cn。若每种魔法石只有一件,问能装入的最大总价值。
输入格式
第一行为两个整数m和n,以下n行中,每行两个整数Wi,Ci,分别代表第i件物品的重量和价值。
输出格式
输出一个整数,即最大价值。
样例
样例输入
样例输出
这个问题是经典的01背包问题,我们需要在不超过背包承重的情况下,选择若干魔法石使得总价值最大。采用动态规划解法,思路如下:
核心思路
- 状态定义:
dp[j]表示背包容量为j时能装入的最大价值 - 状态转移:考虑每个物品时,有两种选择:
- 不放入:
dp[j]保持不变 - 放入:
dp[j] = dp[j - w] + c(需满足j >= w)
- 不放入:
- 状态更新:
dp[j] = max(dp[j], dp[j - w] + c)
算法特点
- 时间复杂度:O(n*m)
- 空间复杂度:O(m)
- 使用一维数组优化空间复杂度
- 内层循环倒序更新(确保物品只使用一次)
参考代码:
4.2 完全背包
题目描述
话说张琪曼和李旭琳又发现了一处魔法石矿(运气怎么这么好?各种嫉妒羡慕恨啊),她们有一个最多能装m公斤的背包,现在有n种魔法石,每种的重量分别是W1,W2,…,Wn,每种的价值分别为C1,C2,…,Cn。若每种魔法石的个数足够多,求她们能获得的最大总价值。
输入格式
第一行为两个整数,即m,n。
以后每行为两个整数,表示每块魔法石的重量和价值。
输出格式
获得的最大总价值。
样例
样例输入
样例输出
这个问题是典型的完全背包问题,与01背包问题不同,每种物品可以取无限多个。我们需要在背包容量限制下最大化总价值。
核心思路
- 状态定义:
dp[j]表示背包容量为j时能装入的最大价值 - 状态转移:对于每种物品(重量
w,价值c)dp[j] = max(dp[j], dp[j - w] + c)
- 关键点:内层循环正序更新(确保物品可重复使用)
参考代码:
4.3 多重背包
这个问题是多重背包问题的变种,需要处理物品数量限制的情况。与完全背包不同,每种物品有数量上限si。我们采用动态规划解法,通过二进制优化将多重背包转化为01背包问题。
核心思路
- 状态定义:
dp[j]表示背包容量为j时的最大价值 - 二进制优化:将si个物品拆分为1,2,4...2^k的组合,转化为01背包问题
- 状态转移:
- 不选:
dp[j]保持不变 - 选:
dp[j] = max(dp[j], dp[j - w] + v)
- 不选:
算法特点
- 时间复杂度:O(N×V×logS)
- 空间优化:使用一维数组逆序更新
- 关键点:二进制拆分减少物品数量
参考代码:
4.4 多重背包的二进制优化
例题:
-
问题理解:这是一个多重背包问题,要求在给定容量的背包中选择物品,使得总价值最大。每种物品有数量限制。
-
动态规划定义:使用二维数组
dp,其中dp[i][j]表示前i种物品在容量为j时的最大价值。 -
状态转移:对于每种物品,遍历其所有可能的数量,更新
dp数组。具体为dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]*k] + w[i]*k),其中k表示当前物品的数量。 -
初始化:
dp[0][j] = 0,表示没有物品时背包的价值为0。
参考代码 :
课后思考:在二进制优化的基础上使用单调队列优化
4.5 变式
-
定义变量:
- 使用静态数组
dp来存储动态规划的结果,其中dp[i][j]表示前i个物品在容量为j的背包中能获得的最大价值。
- 使用静态数组
-
输入读取:
- 读取物品数量
N、背包容量(虽题目给出但此问题中未使用)V和最大重量M。 - 读取每个物品的体积
v、重量m和价值w,但注意此问题中仅重量m和价值w被用于动态规划。
- 读取物品数量
-
初始化DP数组:
- 初始化
dp数组为(N+1) x (M+1)的大小,所有元素初始值为0。
- 初始化
-
动态规划转移:
- 遍历每个物品,对于每个物品,再遍历所有可能的重量
j。 - 如果当前物品的重量
m[i-1]大于当前背包重量j,则不放入背包,dp[i][j] = dp[i-1][j]。 - 否则,选择放入或不放入当前物品,取两者中的较大值:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-m[i-1]] + w[i-1])。
- 遍历每个物品,对于每个物品,再遍历所有可能的重量
-
输出结果:
- 输出
dp[N][M],即表示在容量为M的背包中能获得的最大价值。
- 输出
家庭作业
5.博弈类动态规划
例题:
这道题可以用数学思路做(只需要判断其是否能整除3),但是以下我们还是使用动态规划DP解决
思路讲解:
-
问题定义:给定n个硬币排成一条线,两个玩家轮流从右边拿走1或2个硬币,拿到最后一枚硬币的玩家获胜。判断先手玩家是否必胜。
-
动态规划:定义一个布尔类型的静态数组
dp,其中dp[i]表示当有i个硬币时,先手玩家是否必胜。 -
状态转移:对于
dp[i],如果先手玩家可以通过拿走1或2个硬币,使得剩下的硬币数量i-1或i-2时后手玩家无法获胜(即dp[i-1]或dp[i-2]为false),则先手玩家必胜,即dp[i] = true。反之,如果后手玩家在i-1或i-2时都能找到必胜策略,则先手玩家必败,即dp[i] = false。这可以简化为dp[i] = !dp[i-1] || !dp[i-2]。 -
边界条件:
- 当没有硬币时(
i=0),先手玩家无法获胜,即dp[0] = false。 - 当有1个硬币时(
i=1),先手玩家直接拿走,必胜,即dp[1] = true。 - 当有2个硬币时(
i=2),先手玩家拿走1个,后手玩家无论拿1个还是2个(实际上只能拿1个),先手玩家都能拿走最后一个,必胜,即dp[2] = true。
- 当没有硬币时(
参考代码:
6.双序列问题
-
问题理解:
- 给定两个字符串序列X和Y,要求找出它们的最长公共子序列(LCS)。
- 子序列是指从原序列中删除若干元素(不改变顺序)后得到的序列。
-
动态规划思想:
- 使用一个二维数组
dp,其中dp[i][j]表示X的前i个字符和Y的前j个字符的最长公共子序列长度。 - 初始化
dp[0][j]和dp[i][0]为0,因为空序列与任何序列的最长公共子序列长度为0。
- 使用一个二维数组
-
状态转移:
- 若
X[i-1] == Y[j-1],则dp[i][j] = dp[i-1][j-1] + 1,因为当前字符匹配,长度加1。 - 若不匹配,则
dp[i][j] = max(dp[i-1][j], dp[i][j-1]),取不包含当前字符的两种情况中的最大值。
- 若
参考代码:
7.区间DP
-
问题定义:给定一个字符串S,长度是N,目标是找到它最长的回文子序列的长度。
-
动态规划方法:
- 使用二维数组
dp,其中dp[i][j]表示字符串S从位置i到位置j的子串是否为回文串。 - 初始化:所有长度为1的子串都是回文串,即
dp[i][i] = true。
- 使用二维数组
-
状态转移:
- 如果
S[i] == S[j]且j - i <= 2(即子串长度为1或2,且两端字符相等),则dp[i][j] = true。 - 否则,
dp[i][j] = dp[i+1][j-1],即检查去掉两端字符后的子串是否为回文串。
- 如果
-
计算最长回文子序列长度:
- 在状态转移过程中,如果
dp[i][j] = true且子串长度len = j - i + 1大于当前记录的最大长度maxLength,则更新maxLength。
- 在状态转移过程中,如果
参考代码
总结:
核心思想
- 分解子问题
将原问题划分为多个重叠的子问题,通过解决子问题逐步求解原问题。 - 存储子问题解
通过数组或表格存储子问题的解(称为“记忆化”),避免重复计算,提升效率。 - 状态转移方程
定义状态变量(如dp[i]),建立状态间的递推关系(如dp[i] = dp[i-1] + dp[i-2])。

本文介绍了动态规划(DP)的基本概念及典型应用。核心思想是通过分解问题为相互重叠的子问题,利用记忆化存储避免重复计算。文章通过多个例题详细讲解: 一维DP:硬币找零问题,用最少硬币凑金额; 二维DP:网格路径计数问题,计算机器人从左上到右下的路径数; 线性DP:最长递增子序列问题,分别给出O(n²)和O(nlogn)解法; 划分型DP:字符串回文划分问题,求最少回文划分数; 背包问题:包括01背包和完全背包,说明状态定义和转移方程差异。 每种问题都提供代码实现,并强调状态定义、转移方程和边界处理等关键步骤,还系统介绍了动态规划算法在多重背包、博弈论、双序列和区间问题中的应用。针对多重背包问题,重点讲解了二进制优化方法将问题转化为01背包;博弈类问题通过状态转移分析必胜策略;双序列问题采用二维DP求解最长公共子序列;区间DP则用于计算最长回文子序列。各类问题均给出状态定义、转移方程和参考代码,并强调存储子问题解、避免重复计算的核心思想。文章涵盖了动态规划的多种典型应用场景,展示了该算法通过分解子问题、建立状态转移方程来高效解决复杂问题的通用框架。








浙公网安备 33010602011771号