DP 学习笔记
二维 / 多维 DP
知识点
- 状态定义:使用两个或更多维度表示问题的不同阶段或约束。例如
dp[i][j]表示处理了前i个元素,当前容量 / 位置 / 剩余次数为j时的最优值或方案数。 - 典型场景:网格路径(数字三角形)、背包问题、双线程走方格等。
- 转移方程:根据最后一步的可能决策,从之前的状态递推而来。
- 初始化:边界值(
dp[0][j]或dp[i][0])需根据问题设定,通常为 0、∞ 或特殊值。 - 优化技巧(了解):
- 滚动数组:当转移只依赖上一行时,可将第一维压缩成 2 或直接倒序枚举(0/1 背包)。
- 降维:若状态可合并(如方格取数中两路径步数同步),可将四维降为三维。
常用技巧
- 数字三角形:从上往下(
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + a[i][j])或从下往上(避免边界判断)。 - 背包问题:求方案数时
dp[j] = sum(dp[j], dp[j-w[i]]),注意初始化dp[0]=1。 - 多条路径问题:利用步数相同性质,设
dp[k][i1][i2]表示走了k步,第一条路径到达第i1行,第二条到达i2行,列可由k-i算出。
题目题解
P1164 小 A 点菜
题意:有 n 道菜,每道菜价格 a[i],口袋有 m 元,求正好花完 m 元的点菜方案数。每道菜只能点一次。
思路:0/1 背包求方案数。
- 状态定义:
dp[j]表示凑出j元的方法数。 - 初始:
dp[0] = 1(不花钱有一种方案)。 - 转移:对每道菜
i,倒序枚举j从m到a[i],执行dp[j] += dp[j - a[i]]。 - 答案:
dp[m]。
二维理解:
dp[i][j]表示前i道菜凑出j元的方案,有dp[i][j] = dp[i-1][j] + (j>=a[i]? dp[i-1][j-a[i]] : 0)。一维为滚动数组压缩。
P1216 [IOI1994 / USACO1.5] 数字三角形 Number Triangles
题意:给定数字三角形,从顶部出发,每次向下或右下方移动,求路径上数字和的最大值。
思路:经典二维 DP。
- 状态定义:
dp[i][j]表示从顶部走到第i行第j列的最大和。 - 转移:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + a[i][j](注意边界,j=1或j=i时只有一个来源)。 - 答案:
max(dp[n][j])。 - 优化:可自底向上递推,省去求最大值步骤:
dp[i][j] = a[i][j] + max(dp[i+1][j], dp[i+1][j+1]),答案即dp[1][1]。
P1004 [NOIP2000 提高组] 方格取数
题意:N×N 网格,每个格子有一个数字。从 (1,1) 走到 (N,N) 两次(路径可重叠),每次经过的格子数字会被取走(第二次经过时数字已为 0),求两次路径取数总和的最大值。
思路:等价于两人同时从 (1,1) 走到 (N,N),且每一步同时移动。
- 状态定义:设走了
k步,第一条路径当前在(i1, j1),第二条在(i2, j2),有i1+j1 = i2+j2 = k+2(假设起点步数为 0)。可压缩为dp[k][i1][i2]。 - 转移:每人可向下或向右,共 4 种组合。当前步的取值为
val = a[i1][j1] + (i1==i2 && j1==j2 ? 0 : a[i2][j2])。 - 答案:
dp[2*N-2][N][N]。 - 时间复杂度
O(N^3)。
二、区间 DP
知识点
- 状态定义:
dp[l][r]表示区间[l, r]上的最优值(如合并最小代价、最大得分等)。 - 转移方式:通常枚举区间长度
len,再枚举左端点l,计算右端点r = l+len-1。然后枚举分割点k(l ≤ k < r),合并左右子区间。 - 经典递推:
dp[l][r] = min(dp[l][k] + dp[k+1][r]) + cost(l,r)。 - 初始化:
dp[i][i]通常为 0(单点无需合并)。 - 环形处理:将序列复制一倍接到末尾,破环成链,然后对每个长度为
n的区间取最值。
常用技巧
- 石子合并:合并代价常为区间内石子总数,可用前缀和
O(1)计算。 - 记录最优分割点(Knuth 优化)可加速(了解)。
- 对于“消除 / 合并类”游戏(如 248 G),注意只有当相邻数字相等时才能合并,且合并后数字 +1。
题目题解
P1775 石子合并(弱化版)
题意:n 堆石子排成一行,每次合并相邻两堆,代价为两堆石子数之和,求合并为一堆的最小总代价。
思路:基础区间 DP。
- 状态:
dp[i][j]表示合并区间[i, j]的最小代价。 - 转移:
dp[i][j] = min_{i≤k<j}(dp[i][k] + dp[k+1][j]) + sum(i,j),其中sum(i,j)为区间石子总数,可用前缀和快速得到。 - 初始化:
dp[i][i]=0,其余为无穷大。 - 答案:
dp[1][n]。 - 复杂度
O(n^3),数据弱化时可通过。
P1880 [NOI1995] 石子合并
题意:与 P1775 类似,但石子摆成环形。
思路:破环成链。
- 将石子数组复制一份接在后面,得到长度
2n的数组。 - 对每个长度为
n的区间[i, i+n-1]执行与弱化版相同的区间 DP,求出该区间的合并最小代价。 - 答案:
min_{1≤i≤n} dp[i][i+n-1]。 - 同样可求最大值(题目有时要求两者),只需将
min改为max,并注意初始化边界。
P3146 [USACO16OPEN] 248 G
题意:给定一行 n 个正整数(1~40),每次可选相邻两个相等数字 x 合并成 x+1,求最终能得到的最大数字。
思路:区间 DP 变种。
- 状态定义:
dp[i][j]表示区间[i, j]能否完全合并成一个数字,若能则记录该数字;否则为 0。 - 转移:
dp[i][j]可由两个子区间合并得到,即存在分割点k使得dp[i][k] == dp[k+1][j] != 0,此时dp[i][j] = dp[i][k] + 1。 - 初始化:
dp[i][i] = a[i]。 - 答案:所有
dp[i][j]中的最大值。 - 注意:区间长度从小到大枚举,合并时要求左右子区间都完全合并成单一数字且相等。
三、树形 DP
知识点
- 状态定义:以树节点为下标,通常设
dp[u][state]表示以u为根的子树在某种状态(如选或不选u)下的最优值。 - 转移方式:从叶子向上(后序 DFS),通过合并子节点的信息得到父节点。
- 常见模型:
- 树的最大独立集(没有上司的舞会)
- 树的最小点覆盖
- 树形背包(在树上分组背包)
- 树的直径、最大子树和等
常用技巧
- 建图:无向边,用邻接表存储。
- DFS 时避免重复访问父节点,传入
parent参数。 - 对于“选与不选”的问题,设计
dp[u][0]和dp[u][1]。 - 最大子树和:每个节点可“剪枝”掉某些子节点(即不选该子树的贡献),等价于选取一个连通子图,使得权值和最大。
题目题解
P1352 没有上司的舞会
题意:树形结构,每个节点有快乐指数。若选了某个节点,则不能选它的父节点和子节点(即直接相邻的点不能同时选)。求最大快乐指数和。
思路:经典树形 DP。
- 状态:
dp[u][0]表示以u为根的子树中,不选u的最大快乐值;dp[u][1]表示选u的最大快乐值。 - 转移:
dp[u][1] = a[u] + sum( dp[v][0] )(选 u 则子节点都不能选)dp[u][0] = sum( max(dp[v][0], dp[v][1]) )(不选 u,子节点可选可不选)
- 答案:
max(dp[root][0], dp[root][1])。 - 任选一个根(如 1)进行 DFS。
P1122 最大子树和
题意:树上每个节点有整数权值(可正可负)。可以删除任意节点(连同其与父节点的边),但不能删除所有节点(至少保留一个节点)。求剩下的连通块(必须包含根?题目实际是求一棵树的最大权值连通子图,不一定包含原根,但通常以任意节点为根,取最大值)的最大权值和。
思路:树形 DP 求最大子段和。
- 状态定义:
dp[u]表示以u为根的子树中,包含 u 的最大权值和(即选 u 及其部分后代形成的连通块)。 - 转移:
dp[u] = a[u] + sum_{v∈son(u)} max(dp[v], 0)。也就是说,若子节点子树的贡献为正,则加入;否则舍弃(剪枝)。 - 答案:所有节点
dp[u]中的最大值(因为连通块不一定从原根出发,可以以任意节点为根)。 - 注意:初始化
dp[u] = a[u],最后求max(dp[u])。
四、数位 DP
知识点
- 作用:统计区间
[L, R]内满足某种性质的数字个数或数字和。 - 核心思想:按位处理,利用记忆化搜索,状态记录当前处理到第几位、是否受上界限制、是否有前导零等。
- 模板:
dfs(pos, limit, lead, ...),返回从当前位开始构造的所有合法数的贡献。 - 常用优化:记忆化
dp[pos][state]但只在不限位上界(!limit)时使用,因为limit变化少。
常用技巧
- 将
[L, R]转化为[0, R] - [0, L-1]。 - 处理前导零对计数的影响(例如统计数字 0 的出现次数时需单独处理)。
- 对于数字和类问题,记录当前已积累的数字和。
- 若统计每个数码的出现次数,可分别对 0~9 做一次数位 DP,或者在一个 DFS 中同时统计。
题目题解
P2602 [ZJOI2010] 数字计数
题意:给定两个正整数 a, b,求 [a, b] 中所有数字的十进制表示中,每个数码(0~9)各出现了多少次。
思路:分别统计 0~9 每个数码的出现次数。
- 设计
dfs(pos, sum, limit, lead)返回从当前位开始构造的所有数字中,目标数码d出现的总次数。pos:当前处理到的位数(从高位到低位)。sum:已经统计到的目标数码出现次数。limit:是否受到上界限制。lead:是否有前导零。
- 在
pos到达结束位时,返回sum。 - 对每一位枚举 0~9(根据
limit限制),递归累加结果。 - 注意:当
d=0时,前导零不计入统计,需要特殊处理。 - 对每个数码跑一次 DFS,记忆化状态
dp[pos][sum][lead](limit变化时不能用)。
P4999 烦人的数学作业
题意:给定 L, R,求 [L, R] 中所有数字的各位数字之和的总和。例如 [1,12] 的和为 1+2+3+...+9+1+0+1+1+1+2 = ?。
思路:统计每个数字的各位和,然后累加。
- 数位 DP 状态:
dfs(pos, sum, limit),返回从当前位开始构造的所有数字的 各位数字之和的总和。pos:当前位数。sum:已经累计的各位数字之和(当前已确定的高位贡献)。limit:上界限制。
- 记忆化:
dp[pos][sum]只在!limit时记录。 - 转移:枚举当前位数字
i,递归dfs(pos-1, sum+i, limit && i==up),然后将子结果累加到总和中。 - 边界:
pos==0时返回sum。 - 答案:用
solve(R) - solve(L-1)。

浙公网安备 33010602011771号