暑假第三周笔记
第三周总结
本周主要学习了动态规划,下面来分几个板块逐一介绍
Part 背包
引入
在讲何为背包
有一个容量为
在上述例题中,由于每个物体只有两种可能的状态(取与不取),对应二进制中的
背包
解释
例题中已知条件有第
设 DP 状态
考虑转移。假设当前已经处理好了前
由此可以得出状态转移方程:
这里如果直接采用二维数组对状态进行记录,会出现
因为对
务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。
实现
还有一点需要注意的是,很容易写出这样的 错误核心代码:
for (int i = 1; i <= n; i++)
for (int l = 0; l <= W - w[i]; l++)
f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
// 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]], f[i - 1][l] + w[i]),
// f[i][l + w[i]]); 简化而来
这段代码哪里错了呢?枚举顺序错了。
仔细观察代码可以发现:对于当前处理的物品 (事实上,这正是完全背包问题的解法)
为了避免这种情况发生,我们可以改变枚举的顺序,从
因此实际核心代码为
for (int i = 1; i <= n; i++)
for (int l = W; l >= w[i]; l--)
f[l] = max(f[l], f[l - w[i]] + v[i]);
完全背包
解释
完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
我们可以借鉴 0-1 背包的思路,进行状态定义:设
需要注意的是,虽然定义与 0-1 背包类似,但是其状态转移方程与 0-1 背包并不相同。
过程
可以考虑一个朴素的做法:对于第
状态转移方程如下:
考虑做一个简单的优化。可以发现,对于
理由是当我们这样转移时,
与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。如果理解了 0-1 背包的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。
多重背包
多重背包也是 0-1 背包的一个变式。与 0-1 背包的区别在于每种物品有
一个很朴素的想法就是:把「每种物品选
时间复杂度
二进制分组优化
考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。
解释
显然,复杂度中的
在朴素的做法中,
过程
我们可以通过「二进制分组」的方式使拆分方式更加优美。
具体地说就是令
举几个例子:
显然,通过上述拆分方式,可以表示任意
时间复杂度
实现
index = 0;
for (int i = 1; i <= m; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k > c) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}
混合背包
混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取
这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出伪代码:
过程
for (循环物品种类) {
if (是 0 - 1 背包)
套用 0 - 1 背包代码;
else if (是完全背包)
套用完全背包代码;
else if (是多重背包)
套用多重背包代码;
}
二维费用背包
例题:
有
这道题是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(经费、时间),只需在状态中增加一维存放第二种价值即可。
这时候就要注意,再开一维存放物品编号就不合适了,因为容易
分组背包
例题:
有
这种题怎么想呢?其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 0-1 背包就可以了。
再说一说如何进行存储。我们可以将
实现
for (int k = 1; k <= ts; k++)
for (int i = m; i >= 0; i--)
for (int j = 1; j <= cnt[k]; j++)
if (i >= w[t[k][j]])
dp[i] = max(dp[i], dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
这里要注意:一定不能搞错循环顺序,这样才能保证正确性。
杂项
小优化
根据贪心原理,当费用相同时,只需保留价值最高的;当价值一定时,只需保留费用最低的;当有两件物品
背包问题变种
求方案数
对于给定的一个背包容量、物品费用、其他关系等的问题,求装到一定容量的方案总数。
这种问题就是把求最大值换成求和即可。
例如 0-1 背包问题的转移方程就变成了:
初始条件:
因为当容量为
求最优方案总数
要求最优方案总数,我们要对 0-1 背包里的
这样修改之后,每一种 DP 状态都可以用一个
转移方程:
如果
如果
如果
背包的第 k 优解
普通的 0-1 背包是要求最优解,在普通的背包 DP 方法上稍作改动,增加一维用于记录当前状态下的前 k 优解,即可得到求 0-1 背包第
Part 区间
定义
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
令状态
性质
区间 DP 有以下特点:
合并:即将两个或多个部分进行整合,当然也可以反过来;
特征:能将问题分解为能两两合并的形式;
求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
解释
例题
石子合并
题目大意:在一个环上有
需要考虑不在环上,而在一条链上的情况。
令
写出 状态转移方程:
令
怎样进行状态转移
由于计算
怎样处理环
题目中石子围成一个环,而不是一条链,怎么办呢?
方法一:由于石子围成一个环,我们可以枚举分开的位置,将这个环转化成一个链,由于要枚举
方法二:我们将这条链延长两倍,变成
实现
for (len = 1; len <= n; len++)
for (i = 1; i <= 2 * n - 1; i++) {
int j = len + i - 1;
for (k = i; k < j && k <= 2 * n - 1; k++)
f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j] + sum[j] - sum[i - 1]);
}
Part 状压
定义
状压
例题
在
解释
设
对于编号为
设当前行的状态为
设上一行的状态编号为
Part 和 问题
.最长公共子序列( )
给定一个长度为
子序列的定义可以参考 子序列。一个简要的例子:字符串 abcde
与字符串 acde
的公共子序列有 a
、c
、d
、e
、ac
、ad
、ae
、cd
、ce
、de
、ade
、ace
、cde
、acde
,最长公共子序列的长度是 4。
设
对于每个
该做法的时间复杂度为
int a[MAXN], b[MAXM], f[MAXN][MAXM];
int dp() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (a[i] == b[j])
f[i][j] = f[i - 1][j - 1] + 1;
else
f[i][j] = std::max(f[i - 1][j], f[i][j - 1]);
return f[n][m];
}
.最长不下降子序列( )
给定一个长度为
解法
设
计算
容易发现该算法的时间复杂度为
int a[MAXN], d[MAXN];
int dp() {
d[1] = 1;
int ans = 1;
for (int i = 2; i <= n; i++) {
d[i] = 1;
for (int j = 1; j < i; j++)
if (a[j] <= a[i]) {
d[i] = max(d[i], d[j] + 1);
ans = max(ans, d[i]);
}
}
return ans;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!