首页 > 其他分享 >线性 DP

线性 DP

时间:2024-10-23 21:42:40浏览次数:6  
标签:结尾 int 序列 DP ans 线性 长度 dp

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

例题: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数组最后一个有效元素后面,长度加1
        dp[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;
}

标签:结尾,int,序列,DP,ans,线性,长度,dp
From: https://www.cnblogs.com/ronchen/p/18496664

相关文章

  • 排列组合之线性排列
    1、问题1.1袋中取球袋子里有4个球,分别编号为{1,2,3,4},依次取出,按照取出的先后从左至右排列,会得到一个不同的数字(如1234,有点像双色球开奖),求输出所有的数字组合。1.2不重复的数有4个数字{0,1,2,3},问用这4个数字能组成多少种不能的4位数(0123也算,因为我们也可......
  • 数位dp
    数位dp本质是记忆化搜索。\(lim\)为\(1\),表示当前位之前都是最大的数,当前位的大小受限制,不是1~9,是1~up。\(zero\)为\(0\),表示这一位之前为前导0。\(lim\)的转移:lim&&(i==up)。\(zero\)的转移:zero||i。例题1P2602[ZJOI2010]数字计数给定\(a\)和\(b\),......
  • 【旧文重发】MATLAB 通过函数封装一劳永逸地解决线性规划与运输问题的linprog的标准化
    这篇随笔原本是我上实验课时候的笔记,2023年7月曾经在CSDN平台上发布过。今天恰好有朋友跟我问起MATLAB自带的求解器输入很不直观的问题,我打开这个文章发给他的时候发现自己一年前写的LaTeX公式依托答辩,所以重打了一遍。再加上由于CSDN平台的持续摆烂,终于是用不下去......
  • 概率DP
    概率DP是DP中一个非常重要且较难的DP类型。其题型灵活多变,尤其爱与树形DP结合,同时很可能需要各种数据结构优化。其主要考点便是DP方程的建立与维护。由于“概率”二字,许多时候分类讨论与小数运算也是不可避免的。因此,概率DP对选手的逻辑思维与代码能力也有很高的要求,可以说是DP......
  • AtCoder DP Contest 速通指南
    题单链接这是AT之前办的一场DP专题,里面都是很经典的问题,可以帮助大家复习DP的套路,个人感觉对于巩固基础来说质量很高,建议大家去去联系一下,尽量不要看题解。本博客只讨论了绿色及以上难度的题目,下面是我的题解。ICoins设\(f_{i,j}\)表示扔到了第\(i\)个,有\(j\)个......
  • TCP与UDP协议
    (1)TCP协议面向连接、可靠、基于字节的传输,IP报头中协议号为6。一般用于对可靠性要求较高的应用。(2)UDP协议无连接、不可靠、基于报文的传输,IP报头中协议号为17。主机不需维持连接状态具有较高的传输效率,可靠性由应用层来提供。TCP报头结构①源端口和目的端口:传输层与......
  • 杭州 Day 4 上午 状压 dp
    状压一类杂题P1896[SCOI2005]互不侵犯先预处理出一行的所有可能状态\(S\),应当满足\(S\&(S≫1)=0\),因为不能有相邻的国王。用\(f(i,S,j)\)表示考虑了前\(i\)行,第\(i\)行的状态是\(S\),当前已经放了$$个国王的方案数。转移直接枚举第\(i−1\)行的状态\(T......
  • 提高组专题 dp4
    A[PA2021]OddeskidodeskiDP挺显然的,但我推错了……。\[\begin{split}dp_{i+1,j,1}&+=\sum(dp_{i,j,1}+dp_{i,j,0})\timesj\\dp_{i+1,j+1,0}&+=\sumdp_{i,j,1}\times(m-j)\\dp_{i+1,j,0}&+=\sumdp_{i,j,0}\times(m-j)\end{split}\]#include&......
  • 如何隐藏wordpress主题或插件的更新提示
    如何隐藏WordPress主题或插件的更新提示平常在维护WordPress时,有时候会因为一些错误或者兼容性等问题,我们不能马上升级主题或插件到最新的版本,需要保持旧版本,但是这时候会有一个问题就是每次点开后台都会看到非常显眼的小红点,影响后台体验在本文中我们就来说一下如何在不升级的......
  • 高等数学 7.7常系数齐次线性微分方程
    在二阶齐次线性微分方程\[y''+P(x)y'+Q(x)y=0\tag{1}\]中,如果\(y',y\)的系数\(P(x),Q(x)\)均为常数,即\((1)\)式成为\[y''+py'+qy=0\tag{2}\]其中\(p,q\)是常数,那么称\((2)\)为二阶常系数齐次线性微分方程。如果\(p,q\)不全为常数,就称\((1......