常见问题
闫式DP分析法
- 状态表示
- 集合
- 满足一定条件的所有方案
- 属性
- 集合(所有方案)的某种属性(Max、Min、Count等)
- 集合
- 状态计算(集合划分)
- 如何将当前集合划分成多个子集合
状态计算相当于集合的划分:把当前集合划分成若干个子集,使得每个子集的状态可以先算出来,从而推导当前集合状态(曲线救国)
集合划分规则:不重,不漏
特殊情况:属性是 MAX、MIN 的时候,划分的集合是可以重复的
举个例子 A、B、C,先求A、B的最大值,然后求B、C的最大值,最后求两个最大值的最大值,依旧是A、B、C的最大值。例题 ⭐ 897 最长公共子序列
时间复杂度
状态表示数量 * 状态计算量(转移计算量)
如完全背包问题,假定 N 件物品,物品最低体积为 1,背包最大体积容量 V
朴素二维情况:状态表示数量就是 (NV),状态计算量就是物品体积为 1 的时候(最坏情况),最内层循环最多需计算 V 次,时间复杂度(O(NV^2))
状态转移路径
DP本质是在一个拓扑图内找最短路
每个状态(f(i,j))看作一个点,状态的转移看作一条边,把状态的值理解为最短路径长
更新最短路的规则是根据题目来的,如完全背包规则 (f(i,j)=Max(f(i-1,j-k*v[i])+k*w[i]))
DP结束最终会把从 初始状态(起点)到 目标状态 (终点)的 最短路径长 更新出来(生成了一颗最短路径树)
那么 DP求状态转移路径 就变成了在 拓扑图 中找 最短路径 的问题了,沿用 最短路 输出路径的方法就可以找出 状态的转移
方案总结:循环 与 dfs 均可实现
- 逆推:获取最终状态,根据 最短路规则 逆序往前推,直到 起始状态 ,逐一输出每个状态
- 边转移边记录:状态转移过程中,记录每一条边,再利用递推的栈机制,后序输出
注意的点:
- 因为需要记录路径,状态转移方程可能不好被维度优化,具体问题具体分析
- 边转移边记录一般需要开一个与状态表示一致维度的数组,要存储每个状态是从上一层哪一状态转移过来的;而逆推不需要一致,具体可看下题(保存每组从前组的哪个状态转移过来,一维)
来看一道分组背包题1013 机器分配,要求输出每组选择了哪个物品,我给的题解是第一种方案的循环版本
从终点状态逆推,必定有一条边满足最短路规则,记录当前边,并减去当前组物品的体积,接着开始推前一组状态,直到起始状态
int j = m;
for (int i = n; i; i--) {
for (int k = 0; k
可改写成 dfs 的版本
void dfs(int i, int j) {
if (!i) return;
for (int k = 0; k
也可用第二种方案,边转移边记录(辅助下标 cnt 就不需要了)
for (int i = 1; i
输出既可以循环输出(倒着输出),也可以 dfs 逆序输出(正着输出)
void dfs(int i, int j) {
if (!i) return;
int k = path[i][j];
dfs(i - 1, j - k);
cout = 1; i--) {
cout
背包问题初始化
变量声明
一定要根据状态表示的含义,来声明所需变量
如分组背包题 1013 机器分配,最多 10 组,每组 15 个物品,那么定义的常数 N、M 分别表示状态中每维的最大数量(多申请一点)
path 数组用于边转移边记录路径,理所应当和 f 数组是一致维度的大小;写成 path[N][N]
一直找不出问题的屑。
状态初始化
本篇内容只涉及了 至多、恰好 两种情况的问,至少 情况将在提高篇提及
关于 f[0] = 1
的解释,请看 278 01背包 计数
求方案数
二维情况
1、体积至多j,f[0][i] = 1, 0
下标从1开始
如果状态转移涉及 (i-1) 层,DP问题下标最好从1开始(如第一个物品的下标是1),这是经验,基本上所有DP题目都是这样
递归
所有DP问题都可以改用递归,如 285 没有上司的舞会 901 滑雪
背包DP
01背包
(N) 个物品,(V_i) 表示体积,(W_i) 表示价值,每件物品只有一个。求背包装得下的情况下的最大价值多少
- 状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 个物品,且总体积不大于 (j) 的所有选法
- 属性
- (max) (每个选法的总价值的最大值)
- 集合
- 状态计算
- 不含 (i) 的选法 集合,相当于 (f(i-1,j))
- 含 (i) 的选法 集合,相当于 (f(i-1服务器托管网,j-V_i) + W_i)
- 由于所有选法都包含第 (i) 物品,所以先把第 (i) 物品去掉,然后求最大值,然后再把第 (i) 物品加上
- 可能是空集 (j ,朴素代码中需要做判断
- 转移方程 (f(i,j) = max(f(i-1,j),f(i-1,j-V_i) + W_i)),满足 (V_i
朴素、二维
2. 01背包问题 – AcWing题库
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N][N]; // f[0][0~m] 默认都是0;1件物品都没选
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i = v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout
优化、一维
- (f(i))只用到了(f(i-1))这一层,可以用滚动数组来做
- 把 (j) 当做一维数组下标位置,如果从小到大枚举体积,会覆盖上一层数据,因此从大到小枚举体积,另外可以限制 (j) 遍历到 (v[i]) 停止,这样省了判断语句
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i = v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout
完全背包
(N) 个物品,(V_i) 表示体积,(W_i) 表示价值,每件物品无限多个。求背包装得下的情况下的物品最大价值多少
- 状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 个物品,且总体积不大于 (j) 的所有选法
- 属性
- (max) (每个选法的总价值的最大值)
- 集合
- 状态计算
- 按第 (i) 个物品选多少个划分
- 0个:(f(i-1,j))
- k个:(f(i-1,j-k*v[i])+k*w[i])
- 去掉 (k) 个物品 (i) 的体积
- 求 (f(i-1,j-k*v[i]))
- 加上 (k) 个物品 (i) 的价值
- 转移方程 (f(i,j)=f(i-1,j-k*v[i])+k*w[i]) ,满足 (k*v[i]
朴素、二维
3. 完全背包问题 – AcWing题库
#include
#include
using namespace std;
const int N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i
优化、二维
#include
#include
using namespace std;
const int N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i = v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout
优化、一维
01背包、完全背包有相似之处
#include
#include
using namespace std;
const int N = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i
优化重点
01背包和完全背包一维优化的不同点在状态转移方程上
- 如果用上一层状态就要从大到小枚举体积(01背包二维转一维)
- 如果用本层状态就要从小到大枚举体积(完全背包二维转一维)
多重背包
(N) 个物品,(V_i) 表示体积,(W_i) 表示价值,每件物品(X_i)个。求背包装得下的情况下的物品最大价值多少
- 状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 个物品,且总体积不大于 (j) 的所有选法
- 属性
- (max) (每个选法的总价值的最大值)
- 集合
- 状态计算
- 转移方程 (f(i,j) = max(f(i-1,j-v[i] * k)+w[i]*k),kin[0,X_i]),满足 (k*v[i]
- 转移方程 (f(i,j) = max(f(i-1,j-v[i] * k)+w[i]*k),kin[0,X_i]),满足 (k*v[i]
朴素、二维
4. 多重背包问题 I – AcWing题库
#include
#include
using namespace std;
int const N = 1e2 + 10;
int n, m;
int v[N], w[N], x[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i] >> x[i];
for (int i = 1; i
优化、一维⭐
二进制法,将 (X_i) 拆分成 (lceil log_2X_i rceil) 个物品((1,2,4,8,16,…,2^k,C)),将多重背包转变成01背包问题
#include
#include
using namespace std;
int const N = 1000 * 11 + 10;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
int cnt = 0;
for (int i = 1; i > a >> b >> x;
int k = 1;
while (k 0) { // 若 x 还有剩余
cnt++;
v[cnt] = a * x;
w[cnt] = b * x;
}
}
n = cnt;
for (int i = 1; i = v[i]; j--) f[j] = max(f[j], f[j - v[i]] + w[i]);
cout
究极、单调队列⭐
分析问题
6. 多重背包问题 III – AcWing题库
(begin{array}{l}
0
这种情况下,采用转01背包法,时间复杂度是 (1000 * log_220000*20000) = 1.4e4 + 2e4 = 3e8,肯定会TLE
单调队列
先认识一下单调队列是什么 C++算法之旅、05 基础篇 | 第二章 数据结构 – 小能日记
https://leetcode.cn/problems/sliding-window-maximum
#include
#include
#include
#include
using namespace std;
class Solution {
public:
vector maxSlidingWindow(vector& nums, int k) {
vector res;
int q[nums.size()];
memset(q, 0, sizeof q);
int hh = 0, tt = -1;
for (int i = 0; i = k) res.push_back(nums[q[hh]]);
}
return res;
}
};
解决问题
先来看多重背包的朴素二维代码
for (int i = 1; i
可以这样优化,遍历每个物品,然后总体积 (j) 分为 (r) 类 ((r = v[i] – 1))。选择其中一类,如第 (r) 类,那么从 (r) 遍历到 (vt) 的过程,即 int j = r; j 这一过程,由于物品数量是有限的,相当于维护了一个固定宽度的滑动窗口,(j) 每移动一次,对滑动窗口做出滑动、删除、插入、选择四个操作,滑动窗口用单调队列实现,每个物体只需遍历一遍([0,vt]),原先的 (k) 循环被优化掉了,时间复杂度变为 (O(NV))
- 求 (w[i]) 个数,即队内元素 (j’) 与当前 (j) 的差值 除以 (v[i]);也意味着选择了几个 (i) 物品
- 滑动:队头元素是否出界;也就是 (w[i]) 的个数 > (s[i]) ,选择了大于 (s[i]) 个物品
- 删除:维持单调队列特性;判断队尾每个 (j’) 对应的属性值是否 (j) 当前对应的属性值,True就删除
- 对应的值计算公式
f[i - 1][q[tt]] + (j - q[tt]) / v[i] * w[i]
- 对应的值计算公式
- 插入:将当前 (j) 插入队尾
- 选择:从队头取出 (j’) ,对应的属性值就是 (f(i,j)) 的最大值(滑动窗口 (s[i]) 项里面选择最大一项)
#include
#include
#include
using namespace std;
int const N = 1e3 + 10, M = 2e4 + 10;
int n, vt;
int v[N], w[N], s[N];
int f[N][M];
int q[M];
int main() {
cin.tie(0);
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i] >> s[i];
for (int i = 1; i s[i]) hh++;
// 删除
while (hh
优化代码①
循环已经减少了一层,但空间依旧需要(O(NV)),学着01背包优化的思路,用滚动数组优化一下代码,由于滑动的方向不可变,不能修改 for 循环里的 j,可以将相邻两层状态保存到 f 数组中 f[2][M]
- (i – 1) & 1 就是上一层
- i & 1 就是当前层
#include
#include
#include
using namespace std;
int const N = 1e3 + 10, M = 2e4 + 10;
int n, vt;
int v[N], w[N], s[N];
int f[2][M];
int q[M];
int main() {
cin.tie(0);
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i] >> s[i];
for (int i = 1; i s[i]) hh++;
// 删除
while (hh
优化代码②
不用滚动数组也可以,声明一个跟 f[M] 同等大小的数组 backup[M],用于保存上一个物品 i 的所有状态。新一层循环每次都把上一层循环的 f 数组拷贝到 backup 数组中,这样新一层 i 就可以用到上一层 i 的状态了
#include
#include
#include
using namespace std;
int const N = 1e3 + 10, M = 2e4 + 10;
int n, vt;
int v[N], w[N], s[N];
int f[M], backup[M];
int q[M];
int main() {
cin.tie(0);
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i] >> s[i];
for (int i = 1; i s[i]) hh++;
// 删除
while (hh
分组背包
(N) 个分组,每个分组里有(X)个物品,(V_i) 表示体积,(W_i) 表示价值,每组物品里选一个。求背包装得下的情况下的物品最大价值多少
- 状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 组物品,且总体积不大于 (j) 的所有选法
- 属性
- (max) (每个选法的总价值的最大值)
- 集合
- 状态计算
- 不选第 i 组物品、选第 i 组第 1 个物品、选第 i 组第 2 个物品、…选第 i 组最后一个物品
- 不选第 i 组物品:(f(i-1,j))
- 选第 i 组物品第 k 个物品:(f(i-1,j-v[i,k])+w[i,k])
- 可能是空集 (j ,朴素代码中需要做判断
- 转移方程 (f(i,j) = max(f(i-1,j),f(i-1,j-v[i,k])+w[i,k]),kin [1,X_i]),满足 (v[i,k]
朴素、二维
#include
#include
using namespace std;
int const N = 1e2 + 10;
int n, m;
int v[N][N], w[N][N], x[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i > x[i];
// 这里与上面状态转移方程不同,下标从0开始,个人喜好
for (int j = 0; j > v[i][j] >> w[i][j];
}
}
for (int i = 1; i
优化、一维
#include
#include
using namespace std;
int const N = 1e2 + 10;
int n, m;
int v[N][N], w[N][N], x[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i > x[i];
for (int j = 0; j > v[i][j] >> w[i][j];
}
}
for (int i = 1; i = 0; j--)
for (int k = 0; k
混合背包
物品分类
7. 混合背包问题 – AcWing题库
判断当前物品是哪个类的,然后转移的时候按照各个类的逻辑去转移。本题中的多重背包还可以拆分成 01 背包,所以只需要判断两个类
#include
#include
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n, vt;
int f[N];
struct Thing {
int kind;
int v, w;
};
vector things;
int main() {
cin >> n >> vt;
// 分类
for (int i = 0; i > v >> w >> s;
if (s 0) things.push_back({-1, v * s, w * s});
}
}
for (auto thing : things) {
if (thing.kind = thing.v; j--) {
f[j] = max(f[j], f[j - thing.v] + thing.w);
}
} else if (thing.kind == 0) {
for (int j = thing.v; j
二维费用
体积重量
8. 二维费用的背包问题 – AcWing题库
涉及到了体积与重量,将原先01背包优化后的 (f(i)) 变成 (f(i,j))
- (f(i)) 总体积不超过 (i) 的最大价值
- (f(i,j)) 总体积不超过 (i) 且总重量不超过 (j) 的最大价值
时间复杂度 (O(NVM));因为要用到上一层状态,所以是 (i,j) 都是从大到小枚举;读入物品信息和更新一层状态可以放在一起进行
#include
#include
#include
using namespace std;
int const N = 110;
int n, m, v;
int f[N][N];
int main() {
cin >> n >> v >> m;
for (int i = 0; i > vx >> mx >> wx;
for (int j = v; j >= vx; j--) {
for (int k = m; k >= mx; k--) {
f[j][k] = max(f[j][k], f[j - vx][k - mx] + wx);
}
}
}
cout
方案计数⭐
朴素、二维
11. 背包问题求方案数 – AcWing题库
- 状态表示 (g(i,j))
- 集合
- 考虑前 (i) 件物品,总体积恰好为 (j),且总价值最大的所有选法
- 属性
- (count)
- 集合
- 状态计算
- 选 i 的价值的是最大的
- (g(i,j)=g(i-1,j-v))
- 不选 i 的价值是最大的
- (g(i,j)=g(i-1,j))
- 选 i 和不选 i 的价值都是最大的
- (g(i,j)=g(i-1,j-v)+g(i-1,j))
- 选 i 的价值的是最大的
如何判断状态计算的三种情况:根据已更新完的 (f(i,j)) 来判断,举个例子,如果 (f(i,j)) 从 (f(i-1,j)) 转移过来,那可能就是(不选 i ,选 i 和不选 i )这两种情况,接着判断就行
g 数组更新完后需要遍历最后一层与最大总价值(f(i,j))相同的各项:即考虑前 i 个物品,总体积恰好为 ([0,j]) 的最大总价值的方案数的和
#include
#include
#include
using namespace std;
const int N = 1e3 + 10;
int v[N], w[N];
int f[N][N];
int g[N][N];
int n, vt;
int mod = 1e9 + 7;
int main() {
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i
优化、一维
可以发现 g 数组的更新都是根据 f 数组状态更新的路径来的,两个循环可以放在一块。同时可以用一维数组优化
#include
#include
#include
using namespace std;
const int N = 1e3 + 10;
int v[N], w[N];
int f[N];
int g[N];
int n, vt;
int mod = 1e9 + 7;
int main() {
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i];
g[0] = 1;
for (int i = 1; i = v[i]; j--) {
int temp = max(f[j], f[j - v[i]] + w[i]), c = 0;
if (temp == f[j]) c = (c + g[j]) % mod;
if (temp == f[j - v[i]] + w[i]) c = (c + g[j - v[i]]) % mod;
f[j] = temp, g[j] = c;
}
}
int res = 0;
for (int j = 0; j
方案记录
12. 背包问题求具体方案 – AcWing题库
所有背包问题都可以把方案数出来,相当于求状态转移路径,本题的难点在于字典序输出
针对第一个物品选择有三种情况,后续物品也按这个思路考虑
- 只能选,必选
- 只能不选,必不选
- 可选可不选,必选(保证题目要求的字典序)
原来的背包DP是从 1 推到 n,从 n 逆推的时候并不能保证字典序,况且可能有多个物品选择方式(看看方案计数问题)
所以需要倒过来从 n 推到 1,此时 (f(1,m)) 就是最大总价值,就可以从 1 开始逆推了
#include
using namespace std;
int const N = 1e3 + 10, V = 1e3 + 10;
int n, vt;
int v[N], w[N];
int f[N][V];
int path[N], cnt;
int main() {
cin >> n >> vt;
for (int i = 1; i > v[i] >> w[i];
}
for (int i = n; i >= 1; i--) {
for (int j = 0; j = v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
cout
练习题
278 01背包 计数
278. 数字组合 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 考虑前 (i) 个物品,总体积恰好等于 (j) 的选法集合
- 属性
- (count)
- 集合
- 状态计算
- 不选第 i 物品的所有方案:(f(i-1,j))
- 选择第 i 物品的所有方案:(f(i-1,j-V_i))
- 转移方程:(f(i,j) = f(i-1,j) + f(i-1,j-V_i))
f[0] = 1 含义
f[0] = 1;
表示从前 0 个物品选出总体积为 0 的方案数为 1;一个物品都不选,此时总体积为0,这也是一种合法的方案
二维写法,即 (f(i,0) = 0 , i in [0,n]),表示从前 (i) 个物品选出总体积为 0 的方案数为 1
如计算转移的状态 (f(i-1,j-s*V_i)) 中,如果恰好 (j-s*V_i =0),代表恰好 (s) 个 (i) 物品能够满足 (j) 的要求,此时是一种合法的方案
题解
#include
#include
using namespace std;
int const N = 1e2 + 10, M = 1e4 + 10;
int n, m;
int v[N];
int f[M];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i];
f[0] = 1;
for (int i = 1; i = v[i]; j--) {
f[j] = f[j] + f[j - v[i]];
}
cout
423 01背包 Max
423. 采药 – AcWing题库
(f(i,j))考虑前 (i) 个草药,采集时间不超过 (j) 的所有方案的最大总价值
#include
#include
using namespace std;
int const N = 1e2 + 10, M = 1e3 + 10;
int n, m;
int v[N], w[N];
int f[M];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i = v[i]; j--) f[j] = max(f[j], f[j - v[i]] + w[i]);
cout
426 01背包 Max
426. 开心的金明 – AcWing题库
(f(i,j)) 考虑前 (i) 个物品,在不超过 (j) 元的情况下,所有方案的价格重要度乘积和的最大值
#include
#include
using namespace std;
int const N = 1e2, M = 3e4 + 10;
int n, m;
int v[N], w[N];
int f[M];
int main() {
cin >> n >> m;
for (int i = 1; i > v[i] >> w[i];
for (int i = 1; i = v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + v[i] * w[i]);
cout
279 完全背包 计数
279. 自然数拆分 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 个物品(每个物品若干个),且总体积恰好等于 (j) 的所有选法
- 属性
- (count)
- 集合
- 状态计算
- 不选:(f(i-1,j))
- 选第 (i) 个 (k) 件物品:(f(i-1,j-V_i*k))
- 转移方程 (f(i,j) = sum f(i-1,j-V_i*k)) 满足 (k*v[i]
以下是优化后的写法
#include
#include
using namespace std;
int const N = 4e3 + 10;
int n;
unsigned f[N];
int main() {
cin >> n;
// 初始化
f[0] = 1;
for (int i = 1; i
587 完全背包 Min
587. 吃蛋糕 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 考虑前 (i) 个物品,总体积恰好等于 (j) 的所有方案
- 属性
- (min) (每个方案的元素个数)
- 集合
- 状态计算
- 不选第 i 个:(f(i-1,j))
- 选第 i 个 k 件物品:(f(i-1,j-V_i*k) + k)
- 转移方程 (f(i,j) = min(f(i-1,j-V_i*k) + k))
- (f(0,0)) 为 0(什么都不选元素个数为0);其他 (f(i,j)) 全部 INF(Min);物品数量 (lfloor sqrt(N) rfloor)
朴素写法 (优化写法在后面)
状态数量 (nlog_2n) ,状态转移计算 (n) 次(实际比n小),时间复杂度 (O(n^2log_2n)),会TLE
#include
#include
#include
using namespace std;
int const N = 1e2, M = 1e4 + 10;
int t, n;
int f[N][M];
int main() {
cin >> t;
for (int id = 1; id > n;
int m = 1;
while (m * m
优化写法
for (int i = 1; i
一维写法
#include
#include
#include
using namespace std;
int const N = 1e2, M = 1e4 + 10;
int t, n;
int f[M];
int main() {
cin >> t;
for (int id = 1; id > n;
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 1; i
1013⭐分组背包
1013. 机器分配 – AcWing题库
将每个公司当做物品组,如第一个公司(物品组)包括三个物体(分一台、分两台、分三台)
-
第 (i) 公司第 (k) 件物品
- 含义:分给第 (i) 个公司 (k) 台机器
- 体积:(k)
- 价值:(w_{ik})
-
状态表示 (f(i,j))
- 集合
- 只考虑前 (i) 组物品,且总体积不大于 (j) 的所有选法
- 属性
- (max) (每个选法的总价值的最大值)
- 集合
-
状态计算
- 不选第 i 组物品、选第 i 组第 1 个物品、选第 i 组第 2 个物品、…选第 i 组最后一个物品
- 不选第 i 组物品:(f(i-1,j))
- 选第 i 组物品第 k 个物品:(f(i-1,j-v[i,k])+w[i,k])
题目还要求输出每个公司选择了哪个物品,是求状态转移路径问题
#include
#include
#include
using namespace std;
const int N = 11, M = 16;
int n, m;
int w[N][M];
int f[N][M];
int way[N];
int main() {
cin >> n >> m;
for (int i = 1; i > w[i][j];
for (int i = 1; i
线性DP
递推方程是线性关系(如背包问题二维矩阵,是一行一行来求的)
898 数字三角形
898. 数字三角形 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 从起点走到((i,j))的所有路径
- 属性
- 服务器托管网(max) (每个路径的长度)
- 集合
- 状态计算
- 来自左上方的一类、来自右上方的一类
- 来自左上方:(f(i-1,j-1)+a[i,j])
- 来自右上方:(f(i-1,j)+a[i,j])
- 转移方程 (f(i,j) = max(f(i-1,j-1)+a[i,j],f(i-1,j)+a[i,j]))
状态数量 (n^2),转移计算量 (O(1)),时间复杂度 (O(n^2))
注意初始化部分,算最右侧点的时候,因为最右点不存在,需要初始化 -INF,左侧点同理
#include
#include
using namespace std;
int const N = 5e2 + 10, INF = 1e9;
int a[N][N];
int f[N][N];
int n;
int main() {
cin >> n;
for (int i = 1; i > a[i][j];
for (int i = 0; i
895 最长上升子序列
895. 最长上升子序列 – AcWing题库
- 状态表示 (f(i))
- 集合
- 以序列第 (i) 个数(下标位置)结尾的所有上升子序列
- 属性
- (max) (每个上升子序列的长度)
- 集合
- 状态计算
- 第 (i) 个数已确定,根据第 (i-1) 个数是序列哪个数(下标位置)来分类
-
(r = 0(只有 i 自己)、1、2、3…i-1),前提是 (a_r
- 转移方程 (f(i) = max(f(r) + 1)),(a_r ,(0
状态数量 n,转移数量 n ,故 (O(n^2))
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n;
int a[N];
int f[N];
int main() {
cin >> n;
for (int i = 1; i > a[i];
for (int i = 1; i
如何保存最长序列
用一个数组存储状态的转移
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n;
int a[N];
int f[N];
int g[N];
int main() {
cin >> n;
for (int i = 1; i > a[i];
for (int i = 1; i res) {
res = f[i];
last = i;
}
}
cout
896⭐最长上升子序列2
896. 最长上升子序列 II – AcWing题库
贪心。定义一个数组,存储所有不同长度上升子序列结尾的最小值,随长度增加,结尾最小值单调递增。每次插入一个 (a_i) 整数二分查找(小于某个数的最大的数),返回长度并更新数组。时间复杂度 (O(nlog_2n))。不是DP做法
#include
#include
using namespace std;
int const N = 1e5 + 10;
int n;
int a[N];
int q[N];
int main() {
cin >> n;
for (int i = 0; i > a[i];
int len = 0;
q[0] = -2e9;
for (int i = 0; i > 1;
if (q[mid]
897 最长公共子序列
897. 最长公共子序列 – AcWing题库
-
状态表示 (f(i,j))
- 集合
- 在第一个序列的前 (i) 个字母中出现,且在第二个序列的前 (j) 个字母中出现的所有公共子序列
- 属性
- max (每个公共子序列的长度 )
- 集合
-
状态计算
-
a[i]、b[j] 是否包含在公共子序列当中作为划分依据(四种情况)(不出现和必须出现)
-
00:(f(i-1,j-1)) (这一类情况被包含在01 | 10内)
-
01:(f(i-1,j))
- (f(i-1,j)) 包含 01 的情况,求 max 是可以重复的(求数量不能重复)
-
10:(f(i,j-1)),同理
-
11:(f(i-1,j-1)+1)
- 前提是 a[i] == b[j]
-
一般只考虑 01、10、11 三种情况。状态数量 (n^2) ,状态转移计算 3 次,时间复杂度 (O(n^2))
因为需要从(i-1)层开始算,下标从1开始读
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
cin >> n >> m;
scanf("%s%s", a + 1, b + 1);
for (int i = 1; i
902⭐最短编辑距离
902. 最短编辑距离 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 将a[1~i]变成b[1~j]的所有操作方案
- 属性
- min (每个方案的操作次数)
- 集合
- 状态计算:按最后一次操作分类
- 删 (f(i-1,j) + 1)
- 增 (f(i,j-1) + 1)
- 改 (f(i-1,j-1) + 1/0) (看a[i]是否等于b[j])
状态数量 (n^2) ,状态转移计算 3 次,时间复杂度 (O(n^2))
https://www.acwing.com/solution/content/5607/
相信很多人都是这么考虑问题的:dp[i][j]
为 a 的 [0..i] 转换成 b 的 [0..j] 的最小操作数。
if (删除的情况) dp[i][j] = dp[i-1][j] + 1;
else if (新增的情况) dp[i][j] = dp[i][j-1] + 1;
else if (修改的情况) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i-1][j-1]; // 不需要任何操作, 和上一个状态相同
注意,这种考虑方式不需要写 min,因为你准确的描述了各种情况。
这样想其实是没有毛病的,但是难点在于你无法把 () 中的情况准确的用代码表达出来。
能描述出来的只有 不需要操作 这种情况,若 a[i] == b[j] 则无需修改,其余情况难以准确的描述。
但这题其实求的是 最短编辑距离,突出了一个最短,所以以上情况完全可以归纳为:
// 无需操作
if (a[i] == b[j]) 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;
#include
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
// scanf("%d%s", &n, a + 1);
// scanf("%d%s", &m, b + 1);
cin >> n;
for (int i = 1; i > a[i];
cin >> m;
for (int i = 1; i > b[i];
// 初始化 0
// 只添加
for (int i = 0; i
899 编辑距离
899. 编辑距离 – AcWing题库
注意下标从 1 开始
#include
#include
#include
using namespace std;
const int N = 15, M = 1e3 + 10;
int n, m;
int f[N][N];
char str[M][N];
int edit_distance(char a[], char b[]) {
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 0; i > n >> m;
for (int i = 0; i > (str[i] + 1);
while (m--) {
char s[N];
int limit;
cin >> (s + 1) >> limit;
int res = 0;
for (int i = 0; i
区间DP
282 石子合并
282. 石子合并 – AcWing题库
- 状态表示 (f(i,j)) 第 (i) 堆到第 (j) 堆的区间
- 集合
- 将第 i 堆石子到第 j 堆石子合并成一堆石子的所有合并方式
- 属性
- (min) (每种合并方式的总代价 )
- 集合
- 状态计算
- 以最后一次合并的分界线来分类(两堆合成一堆的分界线)
- 左堆个数,右堆个数:1,k-1 | 2,k-2 | … | k-1,1
- 设分界线为k,先不考虑最后合并步骤,然后再加上最后合并的代价
- 此时分为两堆 ([i,k],[k+1,j])
-
(f(i,j)=Min(f(i,k)+f(k+1,j)+s[j]-s[i-1])) ,后部分是前缀和求区间和
- 左堆合并最小代价+右堆合并最小代价+最后合并的代价
- (kin[i,j-1])
状态数量 (n^2) ,状态转移计算 n 次,时间复杂度 (O(n^3))。按区间长度从小到大枚举,从2到n
#include
#include
using namespace std;
int const N = 3e2 + 10;
int n;
int s[N];
int f[N][N];
int main() {
cin >> n;
for (int i = 1; i > s[i];
for (int i = 1; i
计数DP
900⭐整数划分
900. 整数划分 – AcWing题库
通用解法
- 状态表示 (f(i,j))
- 集合
- 考虑前 (i) 个物品,物品总体积恰好为 j 的所有方案。完全背包
- 属性
- (count)
- 集合
- 状态计算
- 不选第 i 个:(f(i-1,j))
- 选 i 的 k 个:(f(i-1,j-V_i*k)),优化为 (f(i,j-V_i))
- 转移方程:(f(i,j) = f(i-1,j) + f(i,j-V_i))
#include
#include
using namespace std;
int const N = 1e3 + 10;
int n;
int v[N];
unsigned f[N];
int main() {
cin >> n;
f[0] = 1;
unsigned mod = (1e9 + 7);
for (int i = 1; i
另类解法
- 状态表示 (f(i,j))
- 集合
- 总和是 (i) ,并且恰好表示成 (j) 个数的和的所有方案
- 属性
- (count)
- 集合
- 状态计算
- j 个数里最小值是 1
- (f(i-1,j-1))
- j 个数里最小值大于 1
- 把每一个数都减去一个1
- (f(i-j,j))
- (f(i,j) = f(i-1,j-1) +f(i-j,j))
- j 个数里最小值是 1
#include
#include
using namespace std;
unsigned mod = (1e9 + 7);
int const N = 1e3 + 10;
int n;
int f[N][N];
int main() {
cin >> n;
f[0][0] = 1;
for (int i = 1; i
状态压缩DP
用一个 (n) 位二进制数,每一位表示一个物品,0/1 表示不同的状态。因此可以用 ([0,2^n − 1]) 中的所有数来枚举全部状态
291 蒙德里安的梦想
291. 蒙德里安的梦想 – AcWing题库、
摆放长方形的时候,先放横着的,再放竖着的,确保能放满。总方案数等于只放横着小方块的合法方案数
- 状态表示 (f(i,j))
- 集合
- 已将前 i -1 列摆好,且从第 i − 1 列伸出到第 i 列的状态是 j 的所有方案
- 状态 j 是二进制串,当前列每个格子的状态如 101001(1代表前一列该行放了长方形)
- 属性
- (count)
- 集合
- 状态计算
- 第 i – 1 列状态 k ,第 i 列状态 j
- 转移方程 (f(i,j) = sum f(i-1,k))
- j 与 k 需满足
- 没有重行
(j & k) == 0
- k 与 j 并集后的状态连续 0 数量不是奇数
st[j | k]
- 没有重行
- 预处理
j | k
所有可能的状态,也就是 ([1,2^n)),判断每个状态是否出现了奇数个连续0,若出现 st[i] 设置为false -
f[0][0] = 1
根据状态表示,-1 层并不存在,横着摆放长方形个数为 0,那么只有竖着摆放一个方案 -
f[m][0]
是最终结果,此时摆放好了 ([0,m-1]) 层,总共 m 层,第 m 层不能横着放长方形,因此 j 为 0
#include
#include
#include
using namespace std;
const int N = 12, M = 1 > n >> m, n || m) {
memset(f, 0, sizeof f);
// 预处理,列举所有状态,连续0为奇数的状态置为True
for (int i = 0; i > j & 1) {
if (cnt & 1) {
st[i] = false;
break;
}
cnt = 0;
} else
cnt++;
}
if (cnt & 1) st[i] = false;
}
// DP
f[0][0] = 1;
for (int i = 1; i
91 最短Hamilton路径
91. 最短Hamilton路径 – AcWing题库
朴素做法时间复杂度 (O(n!*n)):最坏情况下全排列,然后每个排列算一遍长度,肯定TLE
- 状态表示 (f(i,j))
- 集合
- 所有从 (0) 点走到 (j) 点,走过的所有点是 (i) 的所有路径
- (i) 是二进制数,每一位表示当前点是不是走过
- 属性
- (min) (每个路径的长度)
- 集合
- 状态计算
- 枚举倒数第二个点是哪个点 (kin[0,n-1])
- (f(i-{ j },k)) 所有从 0 走到 k 且不经过 j 的点的所有路径的最短距离
- 转移方程 (f(i,j) = min(f(i-{ j },k) + a(k,j)))
- 从 0 开始走,0 点在二进制1号位,所以
f[1][0] = 0
#include
using namespace std;
const int N = 20, M = 1 > n;
for (int i = 0; i > w[i][j];
}
// 求min,初始化
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
for (int i = 0; i > j & 1)
for (int k = 0; k > k & 1)
f[i][j] = min(f[i][j], f[i - (1
树形DP⭐
285 没有上司的舞会
AcWing 285. 没有上司的舞会 – AcWing
- 状态表示 (f(u,0)) (f(u,1))
- 集合
- (f(u,0)) 以 u 为根的子树中选择,并且不选 u 这个点的所有方案
- (f(u,1)) 以 u 为根的子树中选择,并且选 u 这个点的所有方案
- 属性
- (max) (每个方案的快乐指数)
- 集合
- 状态计算 ((si) 为 u 的所有儿子)
- (f(u,0) = sum Max(f(si,0),f(si,1)))
- (f(u,1) = sum f(si,0))
所有节点总儿子数量等于边数 n – 1 ,整个问题时间复杂度 (O(n))
#include
#include
#include
using namespace std;
int const N = 6e3 + 10;
int n;
int happy[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool has_father[N];
void add(int a, int b) {
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs(int u) {
f[u][1] = happy[u];
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main() {
cin >> n;
for (int i = 1; i > happy[i];
memset(h, -1, sizeof h);
for (int i = 0; i > a >> b;
has_father[a] = true;
add(b, a);
}
int root = 1;
while (has_father[root]) root++;
dfs(root);
cout
记忆化搜索
每一道动态规划问题都可以用递归方式来写(如上面的树形DP)
901 滑雪
901. 滑雪 – AcWing题库
- 状态表示 (f(i,j))
- 集合
- 从 (i,j) 开始滑的所有路径
- 属性
- (max) (每个路径的长度)
- 集合
- 状态计算
- 按第一步往哪个方向滑分为四类(即先不考虑第一步,再考虑第一步)
- 往右划: (f(i,j+1) + 1) ,其他同理(要考虑存在条件,滑到点高度小于当前点)
同时递归不应该存在环。题目能满足要求(点与点的高度差关系)
#include
#include
#include
using namespace std;
int const N = 3e2 + 10;
int n, m;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int dp(int x, int y) {
int &v = f[x][y];
if (v != -1) return v;
v = 1; // 不能走,路径为1
for (int i = 0; i = 1 && nx = 1 && ny > n >> m;
for (int i = 1; i > h[i][j];
memset(f, -1, sizeof f);
int res = 0;
// 遍历每一个起点的最大值
for (int i = 1; i
参考资料
感谢所有大佬的题解,所有博客知识分享。ORZ
OI Wiki – OI Wiki (oi-wiki.org)
多重背包问题 III【单调队列优化+图示】
AcWing 11. 背包问题求方案数【背包DP求最优方案总数】
背包问题中 体积至多是 j ,恰好是 j ,至少是 j 的初始化问题的研究
AcWing 1013. 机器分配【分组背包+背包DP输出方案—拓扑图分析】 – AcWing
AcWing 291. 蒙德里安的梦想 – AcWing
AcWing 291. 蒙德里安的梦想(详细注释 ) – AcWing
AcWing 91. 最短Hamilton路径(超详解) – AcWing
AcWing 12. 背包问题求具体方案【01背包 + 背包DP输出方案】 – AcWing
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net