线性 DP

news/2024/10/23 21:53:02/文章来源:https://www.cnblogs.com/ronchen/p/18496664

最长上升子序列问题是一个经典的线性动态规划问题。

例题:B3637 最长上升子序列

分析:设原始数组为 \(a\),定义状态 \(dp_i\) 表示以 \(a_i\) 结尾的上升子序列的最大长度。注意这个状态定义中有两个重点,第一个重点是 \(dp_i\) 只维护所有原始序列中以 \(a_i\) 结尾的上升子序列的信息。这样可以发现,对于每个上升子序列,都会唯一被归类到 \(dp\) 的某个状态中。第二个重点是对于所有以 \(a_i\) 结尾的上升子序列,只记录长度最长的那个子序列的长度。这是因为最优子结构性质,如果以 \(a_i\) 结尾有很多上升子序列,肯定是保留最长的那个更划算,因为它后面接数字之后能得到更长的上升子序列。而且这种方式能够满足无后效性,因为如果在所有以 \(a_i\) 结尾的上升子序列后面再接数字,能接哪个数字完全取决于 \(a_i\),跟 \(a_i\) 前面的数无关。所以这种状态定义方式同时满足无后效性和最优子结构。

考虑如何进行状态转移,也就是寻找一个递推关系,用之前计算过的某些 \(dp\) 值来计算 \(dp_i\)。考虑 \(dp_i\) 这个状态要以 \(a_i\) 结尾,只需要关心它能接到前面哪些子序列的后面。一种情况是,自成一段,则长度为 \(1\),那么 \(dp_i = 1\);另一种情况是,对于所有 \(i\) 前面的位置 \(j\),且满足 \(a_j < a_i\) 的,\(dp_i = dp_j + 1\),即在以 \(a_j\) 结尾的最长上升子序列的基础上,再增加一个自己带来的长度 \(1\)。为了使得 \(dp_i\) 的值最大,显然应该对于所有 \(j\),取 \(dp_j + 1\) 的最大值。即 \(dp_i = \max (dp_j + 1)\),其中要满足 \(j < i\) 并且 \(a_j < a_i\)

最终的答案就是所有 \(dp_i\) 中的最大值,因为不能确定整个序列的最长上升子序列是以哪个数结尾的,所以每个数作为结尾都要考虑一遍。本算法的时间复杂度为 \(O(n^2)\):因为要枚举以第 \(i\) 个数结尾的情况去计算 \(dp_i\),因此需要枚举 \(n\) 次;而在计算每个 \(dp_i\) 时,又需要把 \(i\) 前面的每个位置 \(j\) 枚举一遍。

参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 5005;
int a[N], dp[N];
int main()
{int n; scanf("%d", &n);for (int i = 1; i <= n; i++) scanf("%d", &a[i]);int ans = 0;for (int i = 1; i <= n; i++) {dp[i] = 1;for (int j = 1; j < n; j++) {if (a[j] <  a[i]) dp[i] = max(dp[i], dp[j] + 1);}ans = max(ans, dp[i]);}printf("%d\n", ans);return 0;
}

还有一个时间复杂度更低的做法。用 \(dp_i\) 表示长度为 \(i\) 的上升子序列中最小的结尾。注意,这个 \(dp_i\) 的定义与前一种方式不同。如果有多个长度为 \(i\) 的上升子序列,记录所有这样的子序列中结尾最小的那个。这满足最优子结构,因为拥有最小结尾的上升子序列,更有可能被后面的数接上,形成更长的上升子序列。

在一开始,只考虑 \(a_1\),这时候有唯一的长度为 \(1\) 的上升子序列,它的结尾是 \(a_1\)

假设数组 \(a\) 等于 \([1, 7, 3, 5, 9, 4, 8]\)。接下来,一个数一个数考虑,把数组 \(a\) 中每个数字考虑进来,分析 \(dp\) 数组的变化。下一个数是 \(a_2 = 7\),它可以接在前面的 \(1\) 的后面,形成长度为 \(2\) 的上升子序列,结尾是 \(7\)。因为之前没有过长度为 \(2\) 的上升子序列,所以直接在 \(dp_2\) 位置写入 \(7\)

下一个数是 \(a_3 = 3\),目前长度为 \(1\) 的子序列是以 \(1\) 结尾的,长度为 \(2\) 的子序列最小结尾是 \(7\),那么新来的这个 \(3\) 肯定不能接在 \(7\) 后面,只能接在 \(1\) 后面,得到一个长度为 \(2\) 的上升子序列,结尾是 \(3\),比之前的 \(dp_2 = 7\) 要小,所以修改 \(dp_2 = 3\)

下一个数是 \(a_4 = 5\),它可以接在长度为 \(2\) 结尾为 \(3\) 的子序列后面,得到长度为 \(3\),结尾为 \(5\) 的上升子序列。

下一个数是 \(a_5 = 9\),它可以接在长度为 \(3\) 结尾为 \(5\) 的子序列后面,得到长度为 \(4\),结尾为 \(9\) 的上升子序列。

到目前为止,大概可以总结出一个算法。一个接一个地考虑数组 \(a\) 中的每个数,对于当前的 \(a_i\),首先看它是否比 \(dp\) 中目前最后一个有效元素大,如果是,那么就可以接在最后面,相当于得到了一个更长的子序列,以 \(a_i\) 结尾;如果 \(a_i\) 不比 \(dp\) 最后一个有效元素大,那么就在 \(dp\) 中,从右往左找到最靠右边的、比 \(a_i\) 小的数,接到它的后面。相当于把 \(dp\) 中最靠左的第一个大于或等于 \(a_i\) 的数修改为 \(a_i\)

例如,下一个考虑的数是 \(a_6 = 4\),就会将 \(dp_3\) 替换成 \(4\)

同理,对于 \(a_7 = 8\),它会替换 \(dp_4\)

image

最终,最长上升子序列的长度是 \(4\),并且最小以 \(8\) 结尾。

分析一下这个做法的时间复杂度,对于每个 \(a_i\),要么接在 \(dp\) 的末尾,要么遍历数组 \(dp\) 寻找最靠左的大于或等于 \(a_i\) 的数进行替换,最坏情况下时间复杂度是 \(O(n)\),总的时间复杂度是 \(O(n^2)\),看起来并没有变优。

实际上,可以发现 \(dp\) 是单调的,所以“遍历 \(dp\) 寻找最靠左的大于或等于 \(a_i\) 的数进行替换”这一操作,是不需要完整遍历的,可以在有序数组上进行二分查找,每次查找的时间复杂度变为 \(O(\log n)\),总的时间复杂度为 \(O(n \log n)\)

参考代码
#include <cstdio>
#include <algorithm>
using std::max;
using std::lower_bound;
const int N = 5005;
int a[N], dp[N];
int main()
{int n; scanf("%d", &n);for (int i = 1; i <= n; i++) scanf("%d", &a[i]);int ans = 0; // 记录最长上升子序列的长度for (int i = 1; i <= n; i++) {// 在dp[1]~dp[ans]间进行二分查找int idx = lower_bound(dp + 1, dp + ans + 1, a[i]) - dp; if (idx > ans) ans++; // 可以接在dp数组最后一个有效元素后面,长度加1dp[idx] = a[i]; // 将二分出的位置替换为a[i]}printf("%d\n", ans);return 0;
}

例题:P1020 [NOIP1999 提高组] 导弹拦截

分析:先考虑第 \(1\) 问,只有 \(1\) 套系统的话,最多可以拦截多少导弹。题目要求“每一发炮弹都不能高于前一发的高度”,其实就是找一个最长的子序列,满足子序列中后一个元素不能比前一个大,只能比前一个小或相等,可以称为最长不上升子序列。

题目第 \(2\) 问是需要多少套系统可以拦截所有的导弹,其实是问最少使用多少个不上升子序列可以覆盖整个区间。针对这类问题,有一个 Dilworth 定理。要求这样的子序列最少多少个,等价于求原序列的最长上升子序列的长度

参考代码
#include <cstdio>
#include <algorithm>
using std::lower_bound;
using std::upper_bound;
const int N = 100005;
int a[N], dp[N];
int main()
{int n = 0, x;while (scanf("%d", &x) != -1) {a[++n] = x;}// 第1问// 求最长不上升子序列的长度,相当于倒过来求最长不下降子序列的长度int ans = 0;for (int i = n; i >= 1; i--) {// 注意:最长上升子序列是lower_bound,最长不下降子序列是upper_bound int idx = upper_bound(dp + 1, dp + ans + 1, a[i]) - dp;if (idx > ans) ans++;dp[idx] = a[i];}printf("%d\n", ans);// 第2问// 等价于求最长上升子序列的长度ans = 0;for (int i = 1; i <= n; i++) {int idx = lower_bound(dp + 1, dp + ans + 1, a[i]) - dp;if (idx > ans) ans++;dp[idx] = a[i];}printf("%d\n", ans);return 0;
}

例题:最长公共子序列

给出两个字符串,求最长的这样的子序列,要求满足子序列的每个字符都能在两个原字符串中找到,而且每个字符的先后顺序和原字符串中的先后顺序一致。
例如,两个字符串分别是 abcfbcabfcab,它们的最长公共子序列长度是 \(4\),如 abfc

设两个字符串分别为 \(s1\)\(s2\),长度分别为 \(len1\)\(len2\)。定义二维状态 \(dp_{i,j}\) 表示 \(s1\) 的前 \(i\) 个字符串形成的子串与 \(s2\) 的前 \(j\) 个字符形成的子串的最长公共子序列的长度。

这个状态定义,还是遵循最优子结构的思想。要解决的是两个比较长的字符串之间的问题,对两个字符串各自截取前若干个字符形成的子串,看看子串里面的答案能否计算出来。如果能,把子串延长一些,看看能否转移,最终计算出的 \(dp_{len1,len2}\) 就是想求的结果。

状态转移方程:\(dp_{i,j} = \begin{cases} dp_{i-1,j-1} + 1, & s1_i = s2_j \\ \max (dp_{i,j-1}, dp_{i-1,j}), & s1_i \ne s2_j \end{cases}\)

考虑两个子串的最后一位 \(s1_i\)\(s2_j\),如果它们相等,那么就可以对答案贡献 \(1\) 的长度。\(s1\) 的前 \(i-1\) 个字符与 \(s2\) 的前 \(j-1\) 个字符能形成的最长公共子序列的长度,再接上新贡献的 \(1\),也就是 \(dp_{i-1,j-1} + 1\)

若两个子串的最后一位 \(s1_i\)\(s2_j\) 不想等,既然它们不能配对为答案做出贡献,不如丢弃其中的某一个。如丢弃 \(s2\) 的第 \(j\) 个字符,看 \(s1\) 的前 \(i\) 个字符与 \(s2\) 的前 \(j-1\) 个字符能够形成的答案是多少,再考虑 \(s1\) 的前 \(i-1\) 位和 \(s2\) 的前 \(j\) 位形成的答案是多少,比较这两个里面哪个更大,那么就构成当前的结果,也就是 \(\max (dp_{i,j-1}, dp_{i-1,j})\)

考虑边界情况,容易发现 \(i=0\)\(j=0\) 时是初始状态,显然这些结果都是 \(0\),因为此时至少有其中一个是空串,无法形成公共子序列。

总的时间复杂度是 \(O(n^2)\)

例题:AT_dp_f LCS

分析:本题需要在求最长公共子序列时把这个序列找出来。一个直观的想法是:除了记录每个状态的最长公共子序列的长度,再配一个相应的数组记录每个状态对应的字符串。状态转移时,除了转移长度,也转移相应的字符串。由于涉及到大量的字符串复制,这个做法比较慢,并且要占用很大的空间。

另一个思路是,记录每个状态是转移自前面的哪个状态的,也就是记录每个状态的父亲状态。在状态转移方程中,可以看到,对于 \(dp_{i,j}\),它的值是从 \(dp_{i-1,j-1}, dp_{i-1,j}, dp_{i,j-1}\) 三个中的某一个转移过来的。所以对于每个状态,可以区分这三种转移来源。最后的结果是看 \(dp_{len1, len2}\),则根据该状态是三种转移中的哪一种倒推回去,直到边界条件。在这个过程中,每当发现某个 \(dp_{i,j}\) 的来源是 \(dp_{i-1,j-1}\) 时就说明最长公共子序列中包含 \(s_i\)\(t_j\) 这个字符(因为此时两者相等,取哪个都一样),把这个过程中涉及到的字符连起来倒序输出即为答案(因为第一个连接到的字符实际上是整个最长公共子序列中的最后一个)。

参考代码
#include <cstdio>
#include <cstring>
const int N = 3005;
char s[N], t[N], ans[N];
int dp[N][N], from[N][N];
int main()
{scanf("%s%s", s + 1, t + 1);int lens = strlen(s + 1), lent = strlen(t + 1);for (int i = 1; i <= lens; i++) {for (int j = 1; j <= lent; j++) {if (s[i] == t[j]) {dp[i][j] = dp[i - 1][j - 1] + 1;from[i][j] = 0; } else {if (dp[i - 1][j] > dp[i][j - 1]) {dp[i][j] = dp[i - 1][j];from[i][j] = 1;} else {dp[i][j] = dp[i][j - 1];from[i][j] = 2;}}}}int x = lens, y = lent;int n = 0;while (x > 0 && y > 0) {if (from[x][y] == 0) { // 转移来源标记等于0表示是一次公共字符ans[++n] = s[x];x--; y--;} else if (from[x][y] == 1) {x--;} else {y--;}}for (int i = n; i >= 1; i--) printf("%c", ans[i]);return 0;
}

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

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

相关文章

20222422 2024-2025-1 《网络与系统攻防技术》实验三实验报告

一、实验内容 1、方法对后门实验中的msf编码器进行进一步的探索使用,使用msfvenom指令生成如jar之类的文件,从而尝试达到免杀的目的; 通过Veil等工具对目标程序进行加壳或者其他操作来实现免杀; 使用C+ShellCode编程实现免杀;2、应用 通过组合应用各种技术尽可能地实现恶意…

Java 解析 XML 转换为 Json

我们使用 Java 开发项目时偶尔会需要使用到 Xml 文件的解析, 一般情况下都会使用 DOM4j、SAX、JDOM 等方案,但这些方案比较代码编写较为繁琐。我们经常使用的 Json 进行数据传输或存储,如果能够将 Xml 快速转换为 Json,将会大大减轻我们后续开发和维护的工作量。 本篇博客简…

热力学与统计力学

统计力学 泊松分布 \[P(k,\lambda)=\frac{\lambda e^{-k}}{k!} \]其中\(\lambda\)是期望的事件数,k是观测到的事件数。 玻尔兹曼分布 \[P_i=\frac{e^{-\beta E_i}}{Z} \]其中\(P_i\)是状态i的概率,\(\beta=\frac{1}{KT}\) Z是配分函数\(Z=\sum_j e^{-\beta E_j}\) 麦克斯韦-玻…

20222409 2024-2025-1 《网络与系统攻防技术》实验二实验报告

1.实验内容 1.1 本周学习内容后门技术:学习了后门的定义及其在网络安全中的作用。后门是一种隐秘的进入方式,允许攻击者绕过正常的认证机制,获取系统访问权限。在实验中实践了如何利用后门获取shell。 netcat:可以用于创建TCP/UDP连接,实现远程shell访问和文件传输。 soca…

经典力学

经典力学 概述 包括运动学和动力学,附加一套分析力学的语言其实就是这一部分的全部核心了。利用最基础的力、能量、动量、速度、加速度等概念再加上目前的这些基本定理自己就可以解决所有的经典力学问题。不过应试的时候还是需要我们去背记一些模型甚至是公式以便加快解题速度…

宝塔面板安装教程

安装前请确保是【全新的机器】,没有安装其他任何环境,否则会影响您的业务使用! 填写好服务器信息,点击“立即安装到服务器”即全自动完成安装,在安装过程中请勿刷新页面! 数据传输过程中加密处理,不保存任何账号密码信息,请放心使用。 系统兼容性推荐:CentOS 7.x >…

FEE-Frontiers in Ecology and Evolution

Frontiers in Ecology and Evolution是一本经同行评议的基础科学和应用科学研究期刊,为自然和人类世界提供生态学和进化的见解。@目录一、征稿简介二、重要信息三、服务简述四、投稿须知 一、征稿简介二、重要信息期刊官网:https://ais.cn/u/3eEJNv三、服务简述 Frontiers in…

实验2 C++

任务1: t.h1 #pragma once2 3 #include <string>4 5 // 类T: 声明6 class T {7 // 对象属性、方法8 public:9 T(int x = 0, int y = 0); // 普通构造函数 10 T(const T &t); // 复制构造函数 11 T(T &&t); // 移动构造函数 12 ~T(…

FPS-Frontiers in Plant Science

Frontiers in Plant Science是在植物研究领域领先的期刊,通过出版严格的同行评审来推动我们对于植物生物学这一领域认知的进步,这本多学科开放获取期刊处于向全球研究人员、学者、政策制定者和公众传播和交流科学知识和有影响力的发现的最前沿。从作物分子遗传学、细胞生物学…

PbootCMS后台管理界面布局错乱,样式不正常怎么办

问题描述:后台管理界面布局错乱,样式不正常。 解决方案:检查CSS文件:确保CSS文件路径正确,文件加载正常。 检查文件权限:确保CSS文件的权限设置正确。 清除浏览器缓存:清除浏览器缓存,重新加载页面。 检查Web服务器配置:确保Web服务器配置正确,特别是静态文件的配置。…

cyi 源鲁杯2024第一轮wp

2024“源鲁杯”高校网络安全技能大赛Round 1 Misc [Round 1] hide_png stegsolve黑白通道(需要自己适当调整大小),然后丁真YLCTF{a27f2d1a-9176-42cf-a2b6-1c87b17b98dc} [Round 1] plain_crack 给了build.py和初始的build,压缩后发现两个文件一样,且加密算法为zipcrypto,…

学期2024-2025-1 学号20241424 《计算机基础与程序设计》第5周学习总结

学期2024-2025-1 学号20241424 《计算机基础与程序设计》第5周学习总结 作业信息 |这个作业属于2024-2025-1-计算机基础与程序设计)| |-- |-- | |这个作业要求在|(https://www.cnblogs.com/rocedu/p/9577842.html#WEEK05))| |这个作业的目标|<参考上面的学习总结模板,把学…