插入dp学习笔记

news/2025/1/19 23:12:53/文章来源:https://www.cnblogs.com/keysky/p/18680483

定义

插入 \(\text{dp}\) 适用于计数、求最优解且具有选择、排列元素过程等题目。
插入 \(\text{dp}\) 大致分为两类:

  • 乱搞型:状态定义天马行空,但始终围绕着将新元素插入到旧元素已有集合中
  • 套路型:\(dp_{i, j}\) 表示前 \(i\) 个数,现在构成 \(j\) 个连续段的方案数\(/\)最优解,此外根据实际情况添加状态,转移则是用新元素新建连续段\(/\)合并两个连续段\(/\)扩张连续段左侧或右侧

乱搞型

模板

说是模板,其实这种类型谈不上什么模板,每一题的状态定义几乎都不一样,都有奇奇怪怪的某一维,所以此题也可以视为经典题。
CF466D
定义 \(dp_{i, j}\) 表示考虑完前 \(i\) 个数且前 \(i\) 个数都已推平,尚有 \(j\) 个区间未闭合的方案数。
对于转移,我们有需要满足推平下一个数的条件,以及同一位置不能同时开启\(/\)关闭多于 \(1\) 个区间,根据这个思路去推状态转移式即可。

/*
address:https://codeforces.com/problemset/problem/466/D
AC 2024/12/24 20:45
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7;
const int N = 2005;
inline void trans(int& x, int y) { x = (x + y) % mod; }
int dp[N][N];
int n, h;
int a[N];
int main() {scanf("%d%d", &n, &h);for (int i = 1;i <= n;i++) scanf("%d", &a[i]), a[i] = h - a[i];dp[0][0] = 1;for (int i = 0;i < n;i++)for (int j = 0;j <= i;j++) {int x = a[i + 1];if (j + 1 == x) {trans(dp[i + 1][j + 1], dp[i][j]);              //开启一个新区见trans(dp[i + 1][j], LL(dp[i][j]) * j % mod);    //关闭又开启一个新区间trans(dp[i + 1][j], dp[i][j]);                  //开启一个区间后马上关闭该区间}if (j == x) {if (j > 0) trans(dp[i + 1][j - 1], LL(dp[i][j]) * j % mod); //关闭一个区间trans(dp[i + 1][j], dp[i][j]);                  //什么事都不干}}printf("%d", dp[n][0]);                                     //最后所有区间都关闭了return 0;
}

实例

AT_dp_t
考虑插入一个数时要考虑插入的数与最后一个数的相对大小关系,并不在意绝对数值,同时方案数计算依赖于又多少数满足条件,所以定义 \(dp_{i, j}\) 表示考虑完前 \(i\) 个数,剩下的数有 \(j\) 个数比位置 \(i\) 填的数小的方案数。这时考虑转移,对于当前字符,选择更大\(/\)更小的,让后转移过去,发现转移要 \(O(N)\) ,但使用差分或前缀和优化可以砍掉,最后是 \(O(N^2)\)

/*
address:https://atcoder.jp/contests/dp/tasks/dp_t
AC 2024/12/24 21:11
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3005;
const int mod = 1e9 + 7;
char s[N];
int n;
LL dp[N][N], tmp[N];
inline void trans(LL& x, LL y) { x = (x + y + mod) % mod; }
int main() {scanf("%d%s", &n, s + 1);for (int i = 0;i < n;i++) dp[1][i] = 1;for (int i = 1;i <= n;i++) {fill(tmp, tmp + n + 1, 0);for (int j = 0;j <= n;j++)if (s[i] == '>') trans(tmp[0], dp[i][j]), trans(tmp[j], -dp[i][j]);else trans(tmp[j], dp[i][j]), trans(tmp[n - i], -dp[i][j]);for (int j = 1;j <= n;j++) trans(tmp[j], tmp[j - 1]);for (int j = 0;j <= n;j++) dp[i + 1][j] = tmp[j];}LL ans = 0;for (int i = 0;i < n;i++) trans(ans, dp[n][i]);printf("%lld", ans);return 0;
}

拓展练习

CF626F 可以排序,定义 \(dp_{i, j, k}\) 表示前 \(i\) 个,有 \(j\) 个小组仍未确定,总不平衡度为 \(k\) 的方案数,卡卡能过,也有通过优化砍掉一维时间的做法,都可以。
Code

套路型

先来思考一个问题,怎么用插入dp求 \(n!\) ,其实就是排列 \(n\) 个元素的方案数,怎么求呢?定义 \(dp_{i, j}\) 表示前 \(i\) 个元素构成 \(j\) 个连续段的方案数,根据定义,我们有如下转移:

\[dp_{i, j} \leftarrow dp_{i - 1, j - 1} \times j + dp_{i - 1, j + 1} \times j + dp_{i - 1, j} \times 2j \]

最终答案即为 \(dp_{n, 1}\)
听起来很奇怪,但这是这一类插入 \(\text{dp}\) 的通用套路,屡试不爽。

实例

Seatfriends
先不管相隔的距离,以套路插入 \(\text{dp}\) 计算若不考虑连续段间需至少隔一个空位时的答案,然后枚举连续段数量并用组合数统计插入空位的方案数,同时特判 \(m = 1\) ,不然会出事,具体可参考我的博客 。
Phoenix and Computers
按套路,定义 \(dp_{i, j}\) 表示已开了 \(i\) 台电脑,构成了 \(j\) 个连续段。
考虑转移,对于新建一个连续段,我们可以在 \(j + 1\) 个空里插,所以转移式是

\[dp_{i, j} \times (j + 1) \to dp_{i + 1, j + 1} \]

对于扩展连续段,我们可以直接在左右两侧添加,也可以隔一个位置加,同时把隔的那个位置自动打开,故有以下转移

\[dp_{i, j} \times 2j \to dp_{i + 1, j}, dp_{i + 2, j} \]

对于合并连续段,由于两个连续段间若距离为 \(1\) 的话中间那台电脑就会自动开启,就已经是一个连续段了,与定义不符,故两个连续段间空位数量为 \(2\)\(3\) ,所以有以下转移

\[dp_{i, j} \times 2(j - 1) \to dp_{i + 2, j - 1} \]

固定区间间距离为 \(3\) ,开中间那台。

\[dp_{i, j} \times (j - 1) \to dp_{i + 3, j - 1} \]

这时就有朋友要问了:“作者作者,你为什么在扩展区间和新建区间不考虑区间间的距离?现在又凭什么能固定距离为 \(2\)\(3\) 呢?”
观察一下我们的定义,是没有考虑绝对距离的,只有考虑相对间距,对于两个区间间的距离,我们也就可以随意调整,同时转移式的正确与齐全也带来了一种“自适应性”,能保证不合法的状态一定不会转移到最终状态,例如当 \(n = 3\)\(2, 2\) 是没法转移到 \(3, 1\) 的状态的,所以不用考虑提到的问题。
参考代码:

/*
address:https://codeforces.com/problemset/problem/1515/E
AC 2024/12/28 10:41
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 405;
int mod;
int n;
LL dp[N][N];
inline void trans(LL& x, LL y) { x = (x + y) % mod; }
int main() {scanf("%d%d", &n, &mod);dp[0][0] = 1;for (int i = 0;i < n;i++)for (int j = 0;j <= i;j++) {trans(dp[i + 1][j + 1], dp[i][j] * (j + 1));trans(dp[i + 1][j], dp[i][j] * j << 1);trans(dp[i + 2][j], dp[i][j] * j << 1);if (j > 1) {trans(dp[i + 2][j - 1], dp[i][j] * (j - 1 << 1));trans(dp[i + 3][j - 1], dp[i][j] * (j - 1));}}printf("%lld", dp[n][1]);return 0;
}

Ant Man
先观察一下题目,发现对于题目中的计算公式可以费用提前计算但前提是在当前插入数的对应方向会继续插入数,可题目已经定义了起点和终点,所以除了起点和终点外,其他座椅的左右两侧都一定会继续插入椅子,故放心大胆使用费用提前计算。
定义 \(dp_{i, j}\) 表示将前 \(i\) 把椅子的访问顺序排完,有 \(j\) 个连续段的最小距离。
考虑转移:
段新增:若插入的数不为 \(s\)\(e\) ,则会添加 \(-2x_i + b_i + d_i\) 否则只考虑右\(/\)左侧添加元素贡献,统共是:

\[dp_{i + 1, j + 1} \leftarrow dp_{i, j} - x_{i + 1} + d_{i + 1} (i + 1 = s) \]

\[dp_{i + 1, j + 1} \leftarrow dp_{i, j} - x_{i + 1} + b_{i + 1} (i + 1 = e) \]

\[dp_{i + 1, j + 1} \leftarrow dp_{i, j} - 2x_{i + 1} + d_{i + 1} + b_{i + 1} (i + 1 \ne s \wedge i + 1 \ne e) \]

同时在添加非 \(s\)\(e\) 的数时,当 \(s\)\(e\) 已插入完毕且当前只有 \(1\) 个连续段,再插入连续段是不合法的,所以还要特判 \(i + 1 <= s \vee i + 1 <= e \vee j + 1 > 2\) 才能进行第 \(3\) 个转移。
段扩张,以同样思路进行分析,考虑规避不合法情况,得到以下转移式:

\[dp_{i + 1, j} \leftarrow dp_{i, j} + x_{i + 1} + c_{i + 1} (i + 1 = s) \]

\[dp_{i + 1, j} \leftarrow dp_{i, j} + x_{i + 1} + a_{i + 1} (i + 1 = e) \]

\[dp_{i + 1, j} \leftarrow dp_{i, j} + b_{i + 1} + c_{i + 1} (i + 1 \ne s \wedge i + 1 \ne e) \]

\[dp_{i + 1, j} \leftarrow dp_{i, j} + a_{i + 1} + d_{i + 1} (i + 1 \ne s \wedge i + 1 \ne e) \]

段合并:

\[dp_{i + 1, j - 1} \leftarrow dp_{i, j} + 2x_{i + 1} + a{i + 1} + c_{i + 1} (i + 1 \ne s \wedge i + 1 \ne e) \]

代码:

/*
address:https://codeforces.com/problemset/problem/704/B
AC 2025/1/3 20:59
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5005;
int n, s, e;
int x[N], a[N], b[N], c[N], d[N];
LL dp[N][N];
inline void trans(LL& x, LL y) { x = x < y ? x : y; }
int main() {scanf("%d%d%d", &n, &s, &e);for (int i = 1;i <= n;i++) scanf("%d", &x[i]);for (int i = 1;i <= n;i++) scanf("%d", &a[i]);for (int i = 1;i <= n;i++) scanf("%d", &b[i]);for (int i = 1;i <= n;i++) scanf("%d", &c[i]);for (int i = 1;i <= n;i++) scanf("%d", &d[i]);for (int i = 0;i <= n;i++)for (int j = 0;j <= n;j++) dp[i][j] = 1e18;dp[0][0] = 0;for (int i = 0;i < n;i++)for (int j = 0;j <= i;j++)if (i + 1 == s) {trans(dp[i + 1][j + 1], dp[i][j] - x[i + 1] + d[i + 1]);if (j > 0) trans(dp[i + 1][j], dp[i][j] + x[i + 1] + c[i + 1]);}else if (i + 1 == e) {trans(dp[i + 1][j + 1], dp[i][j] - x[i + 1] + b[i + 1]);if (j > 0) trans(dp[i + 1][j], dp[i][j] + x[i + 1] + a[i + 1]);}else {if (i + 1 <= s || i + 1 <= e || j + 1 > 2) trans(dp[i + 1][j + 1], dp[i][j] - (x[i + 1] << 1) + d[i + 1] + b[i + 1]);if (j > 0) {if (j > 1 || i + 1 < s) trans(dp[i + 1][j], dp[i][j] + b[i + 1] + c[i + 1]);if (j > 1 || i + 1 < e) trans(dp[i + 1][j], dp[i][j] + a[i + 1] + d[i + 1]);}if (j > 1) trans(dp[i + 1][j - 1], dp[i][j] + (x[i + 1] << 1) + a[i + 1] + c[i + 1]);}printf("%lld", dp[n][1]);return 0;
}

Boss

最后给大家隆重介绍插入 \(dp\) 的Boss:UTS Open '21 P7 - April Fools
大致说一下,先排序,定义 \(dp_{i, j, k, 0/1/2}\) 表示前 \(i\) 个数,有 \(j\) 个连续段以 \(\text{MSB}(A_i) - 1\) 结尾,有 \(k\) 个连续段以 \(\text{MSB}(A_i)\) 结尾,结尾区间的结尾为 \(\text{MSB}(A_i)\) / $\text{MSB}(A_i) - 1 $ / \(\le \text{MSB}(A_i) - 2\) 的方案数。分类讨论手推式子转移,省流:\(34\) 个转移,具体看我的博客 。

/*
address:http://vjudge.net/problem/DMOJ-utso21p7
AC 2025/1/10 22:06
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 505;
const int mod = 1e9 + 7;
int n;
int a[N], f[N];
int dp[2][N][N][3];
inline void trans(int& x, const int y, const int z) { x = (x + 1ll * y * z % mod) % mod; }
int main() {scanf("%d", &n);for (int i = 1;i <= n;i++) scanf("%d", &a[i]);sort(a + 1, a + n + 1);for (int i = 1;i <= n;i++)for (int j = 31;j >= 0;j--)if (a[i] >> j & 1) {f[i] = j;break;}dp[1][0][1][0] = 1;for (int i = 1;i < n;i++) {for (int j = 0;j <= i + 1;j++)for (int k = 0;k + j <= i + 1;k++)for (int l = 0;l < 3;l++) dp[i + 1 & 1][j][k][l] = 0;for (int j = 0;j <= i;j++)for (int k = 0;k + j <= i;k++) {const int c0 = dp[i & 1][j][k][0], c1 = dp[i & 1][j][k][1], c2 = dp[i & 1][j][k][2];const int  l = (i & 1) ^ 1;if (f[i + 1] - f[i] == 0) {// newtrans(dp[l][j][k + 1][0], c0, j + k + 1);trans(dp[l][j][k + 1][1], c1, j + k);trans(dp[l][j][k + 1][0], c1, 1);trans(dp[l][j][k + 1][2], c2, j + k + 1);// extendtrans(dp[l][j][k][0], c0, j + k);trans(dp[l][j][k][1], c1, j + k);trans(dp[l][j][k][2], c2, j + k + 1);if (j > 0) {trans(dp[l][j - 1][k + 1][0], c0, j);trans(dp[l][j - 1][k + 1][1], c1, j - 1);trans(dp[l][j - 1][k + 1][0], c1, 1);trans(dp[l][j - 1][k + 1][2], c2, j);}// mergeif (j > 0) {trans(dp[l][j - 1][k][0], c0, j);trans(dp[l][j - 1][k][1], c1, j - 1);trans(dp[l][j - 1][k][2], c2, j);}}if (f[i + 1] - f[i] == 1) {// newif (j == 0) trans(dp[l][k][1][0], c0, 1);if (j == 0) trans(dp[l][k][1][1], c0, k);if (j == 1) trans(dp[l][k][1][2], c1, k + 1);if (j == 0) trans(dp[l][k][1][2], c2, k + 1);// extendif (j == 0) trans(dp[l][k][0][1], c0, k);if (j == 0 && k > 0) trans(dp[l][k - 1][1][1], c0, k - 1);if (j == 0 && k > 0) trans(dp[l][k - 1][1][0], c0, 1);if (j == 1) trans(dp[l][k][0][2], c1, k + 1);if (j == 1 && k > 0) trans(dp[l][k - 1][1][2], c1, k);if (j == 0) trans(dp[l][k][0][2], c2, k + 1);if (j == 0 && k > 0) trans(dp[l][k - 1][1][2], c2, k);// mergeif (k > 0) {if (j == 0) trans(dp[l][k - 1][0][1], c0, k - 1);if (j == 1) trans(dp[l][k - 1][0][2], c1, k);if (j == 0) trans(dp[l][k - 1][0][2], c2, k);}}}const int j = i & 1, k = j ^ 1;if (f[i + 1] - f[i] >= 2) {// newtrans(dp[k][0][1][2], dp[j][1][0][1], 1);trans(dp[k][0][1][2], dp[j][0][1][0], 1);trans(dp[k][0][1][2], dp[j][0][0][2], 1);// extendtrans(dp[k][0][0][2], dp[j][1][0][1], 1);trans(dp[k][0][0][2], dp[j][0][1][0], 1);trans(dp[k][0][0][2], dp[j][0][0][2], 1);}}printf("%d\n", ((dp[n & 1][1][0][1] + dp[n & 1][0][1][0]) % mod + dp[n & 1][0][0][2]) % mod);return 0;
}

总结

插入 \(dp\) 在学之前去做的话做出的概率无限接近于 \(0\) ,因为无论是乱搞型还是套路型的定义都非常地巧妙,同时这一个 \(\text{dp}\) 地可变性很多,也比较耗脑子,需要多刷题归纳总结各种套路。所以说 \(\text{dp}\) 是人类智慧的结晶是有道理的。

宝剑锋从磨砺出,梅花香自苦寒来

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/871910.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Hugging Face功能介绍,及在线体验当前顶级文生图模型Flux

Hugging Face简介 对于非机器学习或深度学习领域的人士来说,Hugging Face这个名字可能并不耳熟。然而,随着近年来大规模模型的迅速崛起,相信大家或多或少都有所接触。如果你对这一领域感兴趣,并在GitHub上查阅过一些开源资料,那么你一定会频繁地看到Hugging Face的身影。例…

[JavaScript] 深入理解流程控制结构

在编程中,流程控制是指控制代码执行顺序的机制。通过流程控制结构,我们可以根据不同的条件执行不同的代码块,实现逻辑判断、循环遍历和跳出循环等功能。常见的流程控制语句有:if-else、switch-case、for、while,以及控制语句break和continue。本文将详细讲解这些控制结构的…

ELF2开发板(rk3588飞凌)

ELF2开发板(飞凌嵌入式) 开箱包裹内容打开包装,你可以看到以下物品一个绿联的usb3.0读卡器、sandisk的32g内存卡(太好了) rk3588 4g+32g emmc版本ELF2开发板 输出为12v 3A的电源适配器(和ipad的充电器外观好像)图1 外包装盒图2 开箱物品内容图3 ELF2开发板正面图4 ELF2开发…

抗沙箱方式列举

前言 研究过免杀的朋友们一定会碰到过自己的🐎今天还能用,明天就被秒的情况。这种情况大多数是被上传到沙箱然后被沙箱检测和分析发现是🐎,进而记录特征让咱们花好几天写的🐎直接废了。为了提高🐎的存活周期,咱们就需要抗沙箱这门技术。 未加抗沙箱 首先,作为演示我…

K-D树及其应用

K-D树及其应用 简介 在单个维度的范围搜索场景下,如:搜索创建时间最靠近某个日期的商品信息。可以通过遍历所有的商品信息,计算每个商品的创建日期的差值,得到差值最小的商品即可,这样每次查询的时间复杂度为 \(O(n)\);或者通过构造一个 BST,通过日期进行比较查询,这样…

基础动态规划讲解

(标题就叫这个吧,我也没什么主意了) 动态规划,要给这个这个东西下个定义,确实不太好下,他是一种基于状态来思考问题的算法思想 用来表示状态的话,那就是dp,(这么说好抽象),就直接说涉及动态规划的题目怎么处理吧 ,这个还是有步骤可行的,就按如下步骤操作 1.寻找子…

遗传算法个人入门笔记

先举一个简单的求解例子: 变量x,y 函数f(x,y) = (x-5)^2 + (y+3)^2 - 5 求最小值。 def test(x,y):return (x - 5)**2 + (y - 3)**2 - 5显然,这个函数在x=5,y=3时取最小值-5。现在我们尝试用遗传算法解决之。 遗传算法主要是模拟生物进化的过程,将每一个值视作一个生物,有…

excel快速定位到某一行

左上角输入框输入:A100定位到第100行

2024秋季学期 电子技术基础期末复习笔记

这期末也太难了吧,15*2,俩超级难的电路,直接给我算麻了电路分析模拟电路

【牛客训练记录】牛客周赛 Round 77

训练情况赛后反思 打一半吃饭去了,C题看到 ax+by=k 的问题,简单的扩欧exgcd没反应过来,简单数论还是不熟悉TAT,D题DSU计算联通块大小时 \(i\) 打成 \(a_i\) 疯狂 RE 被硬控了十几分钟 A题 输出题目所述的第几个字符串即可 #include <bits/stdc++.h> // #define int l…

LIS于LCS

LIS与LCS是动态规划中最常见的两种情况,LIS也就是最长上升子序列,而LCS是最长公共子序列。 在解决这个问题之前,先要明白为什么是序列,举个例子来说明,在数组 [1,2,3,4,5,6]中,[2,3,5]就是其子序列,也就是说,子序列其实就是数组中存在先后顺序,但不强调连续的子数组。…

层次分析法:数学建模

今天学习了层次分析法和数学建模,这里就简单写一下自己的学习新的,参考的资料是B站上的免费网课,老师讲的不错,可以去围观,学习,希望可以拿个奖。https://www.bilibili.com/video/BV1p14y1U7Nr/?spm_id_from=333.337.search-card.all.click 上面这里是链接。本文的大部分…