首页 > 其他分享 >子序列有关问题总结

子序列有关问题总结

时间:2023-10-14 15:48:26浏览次数:30  
标签:总结 int 有关 元素 cin 序列 最长 dp

我们定义子序列为:从原序列中选取若干个元素,按原序列的顺序排列的序列。

1. 最长上升子序列问题

给定一个长为\(n\)的序列\(a\),求其中的最长的上升子序列的大小。

1.1 动态规划做法

设\(dp_i\)为以\(a_i\)结尾的最长的上升子序列的大小,则序列\(a\)上最长的上升子序列的大小为\(\mathop{\max}_{1\le i\le n}dp_i\)。

对于每个\(dp_i\),如果存在\(j < i\)且\(a_j<a_i\),说明可以把\(a_i\)接到\(a_j\)后面形成一个上升序列。此时以\(a_i\)结尾的序列的大小为\(dp_j+1\),所以我们遍历\(j\in [1,i-1]\),有转态转移方程\(dp_i=\mathop{\max}_{1\le j< i,a_j<a_i}dp_j+1\),从而求出\(dp_i\)的最大值。

这个算法的复杂度为\(O(n^2)\)。

模板题代码:

#include<bits/stdc++.h>

using namespace std;
using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> a(n);
    for(int i = 0; i < n; i++) {
        cin >> a[i];
    }

    vector<int> dp(n, 1);
    int ans = 0;
    for(int i = 1; i < n; i++) {
        for(int j = 0; j < i; j++) {
            if(a[j] < a[i]) {
                dp[i] = max(dp[j] + 1, dp[i]);
            }
        }
        ans = max(ans, dp[i]);
    }

    cout << ans << "\n";

    return 0;
}

1.2 贪心+二分做法

设\(f_{len}\)为长度为\(len\)的上升子序列的末尾元素。每当有新元素添加到\(f\)末尾之后,\(len\)的最大长度加1,说明当前上升子序列的最大长度加1。所以,最后的答案就为\(len\)的最大长度。

从前往后遍历序列\(a\)的所有元素,如果\(a_i\)的大于\(f\)的末尾元素,就把\(a_i\)加到\(f\)的末尾。

要想获得最长的\(len\),就要尽量使\(f\)中每位置的元素尽量小,这样之后才有可能有更多的元素添加到\(f\)的末尾。并且容易证明\(f\)是单调的,所以,当\(a_i\)小于等于\(f\)的末尾元素时,就二分找到\(f\)中第一个大于等于\(a_i\)的元素,并用\(a_i\)替换这个元素。

这个做法的复杂度为\(O(n\log n)\)。

模板题代码:

#include<bits/stdc++.h>

using namespace std;
using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> a(n);
    for(int i = 0; i < n; i++) {
        cin >> a[i];
    }

    vector<int> f{0};// f的初始值根据题目给出的数据大小决定
    for(int i = 0; i < n; i++) {
        if(a[i] > f.back()) {
            f.push_back(a[i]);
        } else {
            int p = upper_bound(f.begin(), f.end(), a[i]) - f.begin();
            f[p] = a[i];
        }
    }

    cout << f.size() - 1 << "\n";

    return 0;
}

1.3 树状数组优化dp

现在回过头再看之前动态规划的做法,注意到对于每个\(dp_i\),要找到\(j\in[1,i-1],dp_j\)的最大值,既然是区间最大值问题,那么我们就可以用树状数组进行优化。

设\(fen_i\)保存指定区间的最大值。我们从前往后遍历所有\(a_i\),每次询问\(fen_{a_i-1}\)以及之前节点的最大值,得到的数值加1即为以\(a_i\)结尾的子序列的最大值\(res\)。更新维护答案后,再把\(fen_{a_i}\)的值设为\(res\)。

有几个需要注意的地方,树状数组的大小由原序列数据的范围决定,当范围太大时记得离散化一下。本题是求最大上升子序列,要求严格递增,所以要询问\(fen_{a_i-1}\)以及之前的节点。如果是求非下降子序列,就询问\(fen_{a_i}\)以及之前的节点。

这个做法的复杂度为\(O(n\log n)\)

模板题代码:

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

constexpr int N = 1e6 + 10;
array<int, N> fen;

void update(int x, int k) {
    for(int i = x; i < N; i += i & -i) {
        fen[i] = max(fen[i], k);
    }
}

int query(int x) {
    int res = 0;
    for(int i = x; i > 0; i -= i & -i) {
        res = max(res, fen[i]);
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> a(n);
    for(int i = 0; i < n; i++) {
        cin >> a[i];
    }

    int ans = 0;
    for(int i = 0; i < n; i++) {
        int res = query(a[i] - 1) + 1;
        ans = max(ans, res);
        update(a[i], res);
    }

    cout << ans << "\n";

    return 0;
}

1.4 其它最长子序列问题

除了最长上升子序列问题外,还有最长不下降子序列,最长下降子序列,最长不下降子序列。

若是第一种dp做法,更改一下条件即可。若是第二种二分做法,除了更改条件,还要注意是使用upper_bound还是lower_bound。

若是第三种树状数组优化dp的做法,当求下降或不上升子序列时,从后往前遍历原序列\(a\),否则从前往后遍历。当上升或下降子序列时,为了避免访问重复的元素取得错误的结果,每次询问的应该是\(fen_{i-1}\)以及之前的节点。

1.5 最长子序列问题的扩展

  • 对于dp做法,我们最后可以的到一个答案数组\(dp_i\),表示记录以\(i\)结尾的最长子序列。现给定一个定理:如果有\(i<j\)且\(dp_i\ge dp_j\),那么\(a_i\)和\(a_j\)一定不能形成一个符合条件的子序列。考虑反证法,如果\(a_i\)和\(a_j\)能形成一个符合条件的子序列,根据转态转态方程可知,\(dp_j\)至少比\(dp_i\)大1,矛盾。

    如果把一个序列的所有顺序对或逆序对之间连边,建图。那么根据上面的定理,我们按条件求最长子序列,得到的dp数组值相同的节点一定不相邻。

  • 上面的定理反过来是不一定成立的,即:如果有\(i<j\)且\(dp_i<dp_j\),那么\(a_i\)和\(a_j\)不一定能形成一个符合条件的子序列。因为\(dp_i\)和\(dp_j\)并不能确定\(a_i\)和\(a_j\)的大小关系。假设我们求[10,11,1,2,3]的最长上升子序列,\(dp_2\)和\(dp_5\)分别为2和3,但是11和3不能形成上升子序列。

  • Dilworth定理:对偏序集\(<A,\le>\),设\(A\)中的最长链的长度为\(n\),那么将\(A\)中的元素分成不相交的反链,反链的个数至少是\(n\)。这个定理可以简述为:一个序列中最少不上升子序列的个数为最长上升子序列的长度。这个定理对其它最长子序列问题也适用。

2. 最长公共子序列问题

给定两个长分别为\(n,m\)的排列\(a,b\),求\(a,b\)的最长公共子序列。

2.1 最长公共子序列的大小

设\(dp_{i,j}\)为考虑\(a\)的前\(i\)个元素,\(b\)的前\(j\)个元素的最长公共个子序列的大小,则最终的答案为\(dp_{n,m}\)。

遍历\(a\)和\(b\),如果有\(a_i=b_j\),说明当前求得的最长公共子序列的大小加1,则\(dp_{i,j}=dp_{i-1,j-1}+1\)。否则,当前的最长公共子序列的大小一定是\(dp_{i-1,j}\)或\(dp_{i,j-1}\)的其中一个,则有\(dp_{i,j}=\mathop \max(dp_{i-1,j},dp_{i,j-1})\)。

这个做法的复杂度为\(O(nm)\)。

模板题代码

#include <bits/stdc++.h>

using namespace std;
using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string a, b;
    while(cin >> a >> b) {
        int n = a.size(), m = b.size();
        vector dp(n + 1, vector<int>(m + 1));
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                if(a[i - 1] == b[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        cout << dp[n][m] << "\n";
    }

    return 0;
}

2.2 求最长公共子序列

如图,假设\(a\)为"ABCDEDC",\(b\)为"EADEDAC",下图每个单元\((i,j)\)即为上面的做法中求得的\(dp_{i,j}\)。

我们先从矩阵的右下角开始,如果有\(a_i=b_j\),就把对应的元素加入到答案中,并往左上角移动。否则的话,它的左边和上边就一定有一个数与它相等,就往与它相等的数的方向移动一格。如此移动直到移动到左上角为止。这其实就是把上面动态规划的做法倒过来而已。

这样我们就获得了最长公共子序列了,下图就显示了它的移动路径,并标出了加入到答案的元素。

这个做法的复杂度为\(O(n+m)\)。

image

2.3 公共子序列问题的扩展

  • 假设要求的序列增加到三个,求最长公共子序列的大小还是用dp的做法。设\(dp_{i,j,k}\)为分别考虑三个序列的前\(i\),前\(j\),前\(k\)个元素的最长公共子序列。那么根据两个序列时的做法,有状态转移方程\(dp_{i,j,k}= \begin{cases} dp_{i-1,j-1,k-1}+1 & A_i=B_j=C_k \\ \mathop\max(dp_{i-1,j,k},dp_{i,j-1,k},dp_{1,j,k-1}) & else \\ \end{cases}\)

    此外,求最长公共子序列也是类似的。如果三个位置的元素相等,则加入到答案中,否则移动到相等的数的位置。

  • 如果给出的两个序列中,每个序列的中的元素都没有重复,那么它可以看成求最长上升子序列问题。

    假设序列\(a\)为[3,2,1,4,5],序列\(b\)为[1,2,3,4,5]。给两个序列重新标号, 把3变成A,2变成B……于是最终两个序列变成\(a\)为[A,B,C,D,E],\(b\)为[C,B,A,D,E]。这样标号后,最长公共子序列的长度不会变,但是\(a\)变成了递增的,那么两个序列的公共子序列也是递增的。所以,\(b\)的最长上升子序列的大小,就是最长公共子序列的大小。

    注意:如果\(b\)中的有元素在\(a\)中没有出现,那么它对答案没有贡献,直接忽略即可。

标签:总结,int,有关,元素,cin,序列,最长,dp
From: https://www.cnblogs.com/cenqi/p/17764229.html

相关文章

  • 2023-2024-1 20231421 《计算机基础与程序设计》第三周学习总结
    ------------恢复内容开始------------作业信息作业要求:https://www.cnblogs.com/rocedu/p/9577842.html#WEEK03作业目标:自学《计算机科学概论》第二章和第三章、《c语言程序设计》第二章作业正文:教材学习内容总结一、《计算机科学概论》1,从信息层出发,分别从二进制数值与......
  • 每日总结10.13
    今天是一个愉快的休息日。下午,我去美甲店做了个精致的美甲,让自己感觉焕然一新。之后,和室友一起去了一家喜爱的餐厅用餐,我们聊得很开心,分享了彼此的近况和快乐。晚上回到家后,我选择了放松一下,给自己一段时间来恢复精力。我欣赏一些音乐,泡了一杯香浓的茶,让自己完全放松下来。随后,我......
  • 每日总结10.9
    今天的学习进度很充实。上午参与了工程实训课程,学习了焊接电路板的基本技巧和注意事项,这项实践让我更深入地理解了电路原理和焊接工艺。下午的Java课程则着重于基础知识的巩固和编程实践。我尝试了一些简单的Java编程练习,并成功完成了相关测试。这让我对Java的语法和概念有了更深......
  • 每日总结10.10
    今天的学习经验非常丰富。上午,我参加了算法与数据结构以及马克思主义原理的课程。在算法与数据结构方面,我们探讨了一些常见的数据结构和算法,这对编程和问题解决能力非常有帮助。而在马克思主义原理方面,我深入了解了社会和政治理论,这将有助于我更好地理解社会和历史背景。下午,我继......
  • 每日总结10.11
    今天的学习日程相当充实。上午,我参加了三节英语课,这有助于提高我的英语语言能力,无论是听力、口语还是阅读和写作。英语作为一门国际语言,对我的未来职业发展非常重要,所以全身心投入学习英语是必要的。下午,我在寝室休息并继续学习了Java编程。Java是一门强大的编程语言,具有广泛的应......
  • 每日总结10.12
    今天的学习日程多元而有趣。上午,我首先参加了统一建模语言(UML)课程,这是一门重要的计算机科学课程,用于软件工程和系统设计。通过学习UML,我学到了如何以图形化的方式表示和分析软件系统的结构和行为,这对于未来的软件开发项目将非常有帮助。接下来是体育课,这对保持身体健康和增强体能......
  • # 定义函数,单个自变量+单个序列(独热编码)控制变量 # curve_fit函数要求X中的元素都是
    importnumpyasnpimportpandasaspdfromscipy.optimizeimportcurve_fit#定义函数,单个自变量deffun_exp(X,k):a,x,b=XY=a*np.exp(k*x)+breturnY#读取数据df_test=pd.DataFrame([[300,0,30,300],[3......
  • 博学谷学习记录 自我总结 用心分享 | Docker容器化
    前言容器技术、虚拟化技术已经成为一种被大家广泛认可的服务器资源共享方式,容器技术可以在按需构建操作系统实例的过程当中为系统管理员提供极大的灵活性。由于hypervisor虚拟化技术仍然存在一些性能和资源使用效率方面的问题,因此容器技术(Container)结合虚拟化技术的解决方案正在......
  • 博学谷学习记录 自我总结 用心分享 | JDK源码刨析
    JDK源码:线程并发协调神器CountDownLatch和CyclicBarrier引言我一直认为程序是对于现实世界的逻辑描述,而在现实世界中很多事情都需要各方协调合作才能完成,就好比完成一个平台的交付不可能只靠一个人,而需要研发、测试、产品以及项目经理等不同角色人员进行通力合作才能完成最终的......
  • python实现根据序列ID从fasta文件中删除指定的序列
     001、[root@pc1test1]#lsa.farm.listtest.py[root@pc1test1]#cata.fa##测试fasta>chr1tttcccggg>chr2tttgggjjjcccjjjjjj>chr3ccc>chr4aaaaatt[root@pc1test1]#catrm.list##删除列表chr2chr4[root@p......