子序列有关问题总结

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

1. 最长上升子序列问题

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

1.1 动态规划做法

dpi为以ai结尾的最长的上升子序列的大小,则序列a上最长的上升子序列的大小为max1indpi

对于每个dpi,如果存在j<iaj<ai,说明可以把ai接到aj后面形成一个上升序列。此时以ai结尾的序列的大小为dpj+1,所以我们遍历j[1,i1],有转态转移方程dpi=max1j<i,aj<aidpj+1,从而求出dpi的最大值。

这个算法的复杂度为O(n2)

模板题代码:

#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 贪心+二分做法

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

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

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

这个做法的复杂度为O(nlogn)

模板题代码:

#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

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

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

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

这个做法的复杂度为O(nlogn)

模板题代码:

#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,否则从前往后遍历。当上升或下降子序列时,为了避免访问重复的元素取得错误的结果,每次询问的应该是feni1以及之前的节点。

1.5 最长子序列问题的扩展

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

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

  • 上面的定理反过来是不一定成立的,即:如果有i<jdpi<dpj,那么aiaj不一定能形成一个符合条件的子序列。因为dpidpj并不能确定aiaj的大小关系。假设我们求[10,11,1,2,3]的最长上升子序列,dp2dp5分别为2和3,但是11和3不能形成上升子序列。

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

2. 最长公共子序列问题

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

2.1 最长公共子序列的大小

dpi,j为考虑a的前i个元素,b的前j个元素的最长公共个子序列的大小,则最终的答案为dpn,m

遍历ab,如果有ai=bj,说明当前求得的最长公共子序列的大小加1,则dpi,j=dpi1,j1+1。否则,当前的最长公共子序列的大小一定是dpi1,jdpi,j1的其中一个,则有dpi,j=max(dpi1,j,dpi,j1)

这个做法的复杂度为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)即为上面的做法中求得的dpi,j

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

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

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

image

2.3 公共子序列问题的扩展

  • 假设要求的序列增加到三个,求最长公共子序列的大小还是用dp的做法。设dpi,j,k为分别考虑三个序列的前i,前j,前k个元素的最长公共子序列。那么根据两个序列时的做法,有状态转移方程dpi,j,k={dpi1,j1,k1+1Ai=Bj=Ckmax(dpi1,j,k,dpi,j1,k,dp1,j,k1)else

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

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

    假设序列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中没有出现,那么它对答案没有贡献,直接忽略即可。

posted @   wuyoudexian  阅读(85)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示