最长上升子序列模型

Posted Zeoy-kkk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最长上升子序列模型相关的知识,希望对你有一定的参考价值。

最长上升子序列模型

怪盗基德的滑翔翼

假设城市中一共有\\(n\\)幢建筑排成一条线,每幢建筑的高度各不相同

初始时,怪盗基德可以在任何一幢建筑的顶端

他可以选择一个方向逃跑,但是不能中途改变方向

怪盗基德只能从较高的建筑滑翔到较低的建筑

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

\\(1 \\le n \\le 100\\)

题解:最长上升子序列 \\(O(n^2)\\) : 可以利用贪心优化至\\(O(nlogn)\\)

我们发现其从较高建筑滑翔至较低建筑的过程,并求其最多可以经过多少建筑,这个过程本质上和最长上升子序列问题是一样的,所以实际上我们只需要正序做一次最长上升子序列,倒序做一次最长上升子序列,然后枚举从每一个建筑出发即可

const int N = 1e2 + 10, M = 4e5 + 10;

int n;
int a[N];
int pre_f[N];
int suf_f[N];

void solve()

    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= n; ++i)
        pre_f[i] = suf_f[i] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < i; ++j)
            if (a[i] > a[j])
                pre_f[i] = max(pre_f[i], pre_f[j] + 1);
    for (int i = n; i >= 1; i--)
        for (int j = n; j > i; j--)
            if (a[i] > a[j])
                suf_f[i] = max(suf_f[i], suf_f[j] + 1);
    int ans = -INF;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, pre_f[i], suf_f[i]);
    cout << ans << endl;

合唱队形

\\(n\\) 位同学站成一排,音乐老师要请其中的\\(n-k\\)位同学出列,使得剩下的 \\(k\\) 位同学排成合唱队形。

合唱队形是指这样的一种队形:设 \\(k\\) 位同学从左到右依次编号为 \\(1,2…,k\\) 他们的身高分别为 \\(T_1,T_2,…,T_k\\) 则他们的身高满足 \\(T_1<…<T_i>T_i+1>…>T_k(1≤i≤k)\\)

你的任务是,已知所有 \\(n\\) 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

题解:最长上升子序列

题目中让我们计算最少需要多少个同学出列,我们可以将题目转化为最多有多少位同学可以拍成合唱队形,那么多余的人数就是最少需要出列的同学人数

我们发现最高的同学i左侧是最长上升子序列,右侧是最长下降子序列,那么我们只需要枚举将每一位同学作为合唱队形中最高的同学即可,注意最后两个长度加起来时需要减去1,因为根据容斥,中间最高的那名同学多算了1次

const int N = 1e3 + 10, M = 4e5 + 10;

int n;
int a[N];
int pre_f[N];
int suf_f[N];

void solve()

    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= n; ++i)
        pre_f[i] = suf_f[i] = 1;
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < i; ++j)
            if (a[i] > a[j])
                pre_f[i] = max(pre_f[i], pre_f[j] + 1);
    for (int i = n; i >= 1; i--)
        for (int j = n; j > i; j--)
            if (a[i] > a[j])
                suf_f[i] = max(suf_f[i], suf_f[j] + 1);
    int ans = -INF;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, pre_f[i] + suf_f[i] - 1);
    cout << n - ans << endl;

最大上升子序列和

给你一个子序列,让你求出该序列中的最大上升子序列和

题解:线性\\(DP\\)

  • 状态表示:\\(f[i]\\)代表以第\\(i\\)个数结尾的最长上升子序列和

  • 状态属性:\\(MAX\\)

  • 状态计算:

\\[f[i] = f[i] + f[j],a[i] > a[j] \\wedge j < i \\]

  • 状态初始:\\(f[i] = a[i]\\)

  • 答案呈现:\\(max\\sum f[i]\\)

const int N = 1e3 + 10, M = 4e5 + 10;

int n;
int a[N];
int f[N];

void solve()

    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= n; ++i)
        f[i] = a[i];
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < i; ++j)
            if (a[j] < a[i])
                f[i] = max(f[i], f[j] + a[i]);
    int ans = -INF;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, f[i]);
    cout << ans << endl;

拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于\\(30000\\)的正整数,导弹数不超过\\(1000\\)),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统

题解:贪心 + \\(DP\\): \\(O(nlogn)\\) / \\(Dilworth\\)定理

显然第一个问题是一个非常明显的最长不上升子序列问题,做法两种:一种是\\(dp:O(n^2)\\),另一种是贪心:\\(O(nlongn)\\),我们这里选择的是贪心的方法,不再赘述

我们来想一想第二个问题:

我们将第二个问题转化为:一个序列最少需要用多少个不上升子序列能够将其覆盖掉

也就是说我们需要将每个数分配到一个不上升子序列中,我们考虑贪心解决,类似最长不上升子序列的贪心方法:

  1. 我们利用数组\\(g\\)维护所有不上升子序列的结尾数字
  2. 对于每一个需要分配的数字\\(x\\),我们贪心的将其分配进入所有结尾数字中第一个比\\(x\\)大的不上升序列中
  3. 如果数字\\(x\\)比每一个结尾数字都要大的话,说明\\(x\\)需要自己作为一个不上升子序列,即新建一个不上升子序列

我们发现数组\\(g\\)中的结尾数字单调递增,所以每次分配的位置我们可以利用二分找到,那么最终复杂度为\\(O(nlogn)\\)

我们惊奇的发现其贪心的过程不就是求最长子序列长度的贪心过程嘛,实际上这里存在一个定理:

\\(Dilworth\\)定理:一个序列最少用多少个不上升子序列将其覆盖掉的数量等于该序列最长上升子序列的长度

const int N = 1e3 + 10, M = 4e5 + 10;

int n;
int a[N];
int f[N];
multiset<int> st;

void solve()

    int x;
    while (cin >> x)
        a[++n] = x;
    int len = 0;
    for (int i = 1; i <= n; ++i)
    
        if (len == 0 || (len > 0 && a[i] <= f[len]))
            f[++len] = a[i];
        else if (len > 0 && a[i] > f[len])
        
            int p = upper_bound(f + 1, f + len + 1, a[i], greater<int>()) - f;
            f[p] = a[i];
        
    
    int ans2 = 0;
    for (int i = 1; i <= n; ++i)
    
        if (st.empty() || (st.size() && *(prev(st.end())) < a[i]))
        
            ans2++;
            st.insert(a[i]);
        
        else if (st.size() && *(prev(st.end())) >= a[i])
        
            st.erase(st.lower_bound(a[i]));
            st.insert(a[i]);
        
    
    cout << len << endl;
    cout << ans2 << endl;

导弹防御系统

为了对抗附近恶意国家的威胁,\\(R\\) 国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为 \\(3\\) 和高度为 \\(4\\) 的两发导弹,那么接下来该系统就只能拦截高度大于 \\(4\\) 的导弹

给定即将袭来的一系列\\(n\\)个导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落

\\(1 \\le n \\le 50\\)

题解:\\(DFS\\)求最优解(迭代加深 / 全局最优解) + 贪心 \\(O(n2^50)\\)

我们将题目转化为:一个序列最少需要用多少个上升子序列和下降子序列能够将其覆盖掉

如果说只用一种序列,我们完全可以用拦截导弹的贪心方法解决,但是这里既可以用上升子序列,又可以用下降序列,并且数据范围比较小,所以我们完全可以使用\\(dfs\\)爆搜 + 剪枝解决

  • 首先我们需要明确搜索的顺序,怎样才能枚举完所有的方案?
  1. 对于每一个数,我们枚举该数放在上升序列还是放在下降序列中

  2. 如果该数放在上升序列,那么我们枚举该数放在哪个上升序列中

    如果该数放在下降序列,那么我们枚举该数放在哪个下降序列中

对于上述的搜索顺序,一定能够枚举完所有的合法方案

但是复杂度太高了,我们需要对其剪枝,我们发现我们维护一个所有上升序列结尾数字数组\\(up[]\\)和一个所有下降序列结尾数字数组\\(down[]\\),这样的话我们借用拦截导弹的贪心思想:

  • 如果我们将一个数放在某个上升序列的末尾,那么根据贪心的思想,这个数一定是放在第一个末尾数字比这个数小的上升序列后面,如果我们找不到任何一个上升序列放该数字,说明该数字单独作为一个新的上升序列

  • 如果我们将一个数放在某个下降序列的末尾,那么根据贪心的思想,这个数一定是放在第一个末尾数字比这个数大的下降序列后面,如果我们找不到任何一个下降序列放该数字,说明该数字单独作为一个新的下降序列

  • 那么此时我们发现我们维护的\\(up\\)数组一定是单调递减的,\\(down\\)数组一定是单调递增的,所以我们在搜索时枚举该数放在哪个上升或者下降序列时,只要枚举到\\(up\\)数组中第一个比这个数小的序列末尾或者枚举到\\(down\\)数组中第一个比这个数大的序列末尾即可,这是一个很不错的剪枝

  • 但是我们还需要考虑一个问题,如何在\\(dfs\\)爆搜的过程中得到最小值:

有两种方法:

  1. 迭代加深:一般用于平均答案比较小的情况下,比如说本题,在本题中我们迭代的深度\\(depth\\)代表现在上升序列和下降序列的个数之和,所以说如果我们在某一次迭代过程中上升序列和下降序列之和超过了\\(depth\\),我们可以直接退出迭代,进行下一次迭代,直到迭代成功
  2. 全局最优解:在枚举的所有可行方案中找到全局最优解,但是需要注意剪枝:如果现在的上升序列和下降序列之和已经大于等于了当前最优解,直接退出即可,说明当前解不可能产生全局最优解
//方法一:迭代加深
const int N = 55, M = 4e5 + 10;

int n;
int a[N];
int up[N];
int down[N];

bool dfs(int depth, int u, int num_up, int num_down)

    if (num_up + num_down > depth) 
        return false;
    if (u == n + 1)
        return true;

    // 枚举放入单调上升序列中
    bool flag = false;
    for (int i = 1; i <= num_up; ++i) // up数组单调递减
    
        if (up[i] < a[u])
        
            int t = up[i];
            up[i] = a[u];
            if (dfs(depth, u + 1, num_up, num_down))
                return true;
            up[i] = t;		//还原现场
            flag = true;
            break;			//贪心剪枝,直接可以退出
        
    
    if (!flag) //如果任何一个上升序列都放不进
    
        up[num_up + 1] = a[u];
        if (dfs(depth, u + 1, num_up + 1, num_down))
            return true;
        down[num_down + 1] = 0;
    

    // 枚举放入单调下降序列中
    flag = false;
    for (int i = 1; i <= num_down; ++i) // down数组单调递增
    
        if (down[i] > a[u])
        
            int t = down[i];
            down[i] = a[u];
            if (dfs(depth, u + 1, num_up, num_down))
                return true;
            down[i] = t;	//还原现场
            flag = true;
            break;			//贪心剪枝
        
    
    if (!flag)  //如果任何一个下降序列都放不进
    
        down[num_down + 1] = a[u];
        if (dfs(depth, u + 1, num_up, num_down + 1))
            return true;
        down[num_down + 1] = 0;
    
    return false;


void solve()

    while (cin >> n, n)
    
        for (int i = 1; i <= n; ++i)
            cin >> a[i];
        int depth = 0;
        while (!dfs(depth, 1, 0, 0))	//迭代加深,直到迭代成功
            depth++;
        cout << depth << endl;
    

//方法二:全局最优解
const int N = 55, M = 4e5 + 10;

int n;
int a[N];
int up[N];
int down[N];
int ans;

void dfs(int u, int num_up, int num_down)

    if (num_up + num_down >= ans)		//如果大于当前最优解,直接退出
        return;
    if (u == n + 1)
    
        ans = min(ans, num_up + num_down);
        return;
    

    // 枚举放入单调上升序列中
    bool flag = false;
    for (int i = 1; i <= num_up; ++i) // up数组单调递减
    
        if (up[i] < a[u])
        
            int t = up[i];
            up[i] = a[u];
            dfs(u + 1, num_up, num_down);
            up[i] = t;
            flag = true;
            break;
        
    
    if (!flag)
    
        up[num_up + 1] = a[u];
        dfs(u + 1, num_up + 1, num_down);
        up[num_up + 1] = 0;
    

    // 枚举放入单调下降序列中
    flag = false;
    for (int i = 1; i <= num_down; ++i) // down数组单调递增
    
        if (down[i] > a[u])
        
            int t = down[i];
            down[i] = a[u];
            dfs(u + 1, num_up, num_down);
            down[i] = t;
            flag = true;
            break;
        
    
    if (!flag)
    
        down[num_down + 1] = a[u];
        dfs(u + 1, num_up, num_down + 1);
        down[num_down + 1] = 0;
    


void solve()

    while (cin >> n, n)
    
        for (int i = 1; i <= n; ++i)
            cin >> a[i];
        ans = 100;
        dfs(1, 0, 0);
        cout << ans << endl;
    

最长公共上升子序列

给定长度都为\\(n\\)的序列a,b,求其最长公共上升子序列的长度

题解:线性\\(DP\\) :\\(O(n^2)\\)

  • 状态表示:

\\(f[i][j]\\)代表在\\([1,i]\\)\\([1,j]\\)中的,且以\\(b_j\\)结尾的公共上升子序列的最大长度

  • 状态属性:\\(MAX\\)

  • 状态计算:按照公共上升子序列中\\(a_i\\)是否存在分为两个集合:

  1. \\(a_i\\)不在公共上升子序列中:\\(f[i-1][j]\\)
  2. 如果\\(a_i\\)在公共上升子序列中,必须满足\\(a_i = b_j\\),那么这一集合可以按照\\(b_j\\)接在哪个上升序列的末尾后面继续划分:
    • \\(b_j\\)不接在任何一个上升序列后面:\\(1\\)
    • \\(b_j\\)接在\\(b_1\\)后面\\((b_1 < b_j)\\)\\(f[i-1][1] + 1\\)
    • \\(b_j\\)接在\\(b_2\\)后面\\((b_2 < b_j)\\)\\(f[i-1][2] + 1\\)
    • ......
    • \\(b_j\\)接在\\(b_j-1\\)后面\\((b_j-1 < b_j)\\)\\(f[i-1][j-1] + 1\\)

\\[f[i][j] = f[i-1][j]\\\\ f[i][j] = max(f[i][j],f[i-1][k] + 1),k < j\\ \\ \\wedge \\ \\ b_k<b_j \\ \\ \\wedge \\ \\ a_i = b_j \\]

  • 状态初始: \\(f[i][j] = 0\\)

  • 答案呈现:\\(max\\sum f[n][i]\\)

  • 时间复杂度: \\(O(n^3)\\)

  • 状态优化:我们发现只有\\(a_i = b_j\\)时,我们才会讨论\\(b_j\\)接在哪个上升序列的末尾后面,假设\\(b_j\\)接在末尾为\\(b_k\\)的子序列后面,则\\(b_k < b_j\\),即\\(b_k < a_i\\),所以我们不妨在寻找\\(a_i = b_j\\)的同时顺便维护\\(b_k < a_i\\)中的\\(f[i-1][k] + 1\\)的最大值,优化后时间复杂度为\\(O(n^2)\\)

const int N = 3e3 + 10, M = 4e5 + 10;

int n;
int f[N][N]; // f[i][j]代表所有[1,i]和[1,j]中的,且以b[j]结尾的公共上升子序列的最大长度
int a[N], b[N];

void solve()

    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= n; ++i)
        cin >> b[i];
    for (int i = 1; i <= n; ++i)
    
        int mx = 1;
        for (int j = 1; j <= n; ++j)
        
            f[i][j] = f[i - 1][j];
            if (a[i] == b[j])
                f[i][j] = max(f[i][j], mx);
            else if (a[i] > b[j])
                mx = max(mx, f[i - 1][j] + 1);
        
    
    int ans = -INF;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, f[n][i]);
    cout << ans << endl;

动态规划 最长上升子序列模型——进阶

拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式
共一行,输入导弹依次飞来的高度。

输出格式
第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

数据范围
雷达给出的高度数据是不大于 30000 30000 30000 的正整数,导弹数不超过 1000 1000 1000

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

分析: 这个题有两个小问,第一是问最多能拦截多少导弹,而我们一次能够拦截的导弹要求是后一发不能高于前一发,也就是说要求一个最长非严格递减子序列
第二问拦截所有导弹最少要配备多少套这种导弹拦截系统,也就是求最少有多少个最长非严格递减子序列。这一问我们要使用贪心的策略
策略如下:
从前往后扫每一个数,对于每个数
1.如果现在的所有子序列的结尾都小于当前的数(也就是说当前的导弹无法被现有的任何拦截系统拦截),则创建新的子序列
2.将当前数放在结尾大于等于它的最小的子序列的后面,这样我们就让所有序列剩下的能够放的数字尽可能的多
这样的贪心策略是最优的,简单证明一下。
如何证明两个数相等?
A > = B , B > = A = > A = B A>=B,B>=A =>A=B A>=B,B>=A=>A=B
A A A:贪心所得到的序列的个数
B B B:最优解(最少的序列个数)
A > = B A>=B A>=B是显然成立的,因为B是最优解
证明: B > = A B>=A B>=A:(调整法)
假设最优解对应的方案和当前方案不同。
找到第一个不同的数。

假设我们贪心法的x和最优解的x放在了上图的位置,那么有x<=a<=b,我们显然可以将a后面的一段和b后面的一段交换位置,这样是不会影响最后的最优解的。所以我们经过很多次的变化,我们没有增加子序列的个数,可以将贪心法变换到最优解的情况。所以得到A>=B。所以我们就可以按照这样的贪心策略来求最优解。
实际上这有一个Dilworth定理,可以自行搜索。
实现方法:我们维护一个数组ans,存储每一个序列的最后一个值,每一次增加一个数,如果当前数比ans中的所有数都大,也就是说当前的导弹无法被现有的任何拦截系统拦截,那么我们就要新开一个序列来存这个值。
否则,找到ans中比当前数大的数的最小值,替换ans数组中的这个值为当前数。
不难看出,ans数组是一个单调递增序列,可以使用二分来简化时间复杂度。
而这样的贪心策略和我们的最长递增子序列的贪心求解法 O ( n l o g n ) O(nlogn) O(nlogn)是一样的。所以第二问可以转化为求一个严格最长递增子序列。

代码:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int ans[1005],n,a[1005],k;
int main()
{
    while(~scanf("%d",&a[++n]));
    n--;
    for(int i=1;i<=n;i++){
        int id=upper_bound(ans,ans+k,a[i],greater<int>())-ans;
        if(id<k)ans[id]=a[i];
        else ans[k++]=a[i];
    }
    printf("%d\\n",k);
    k=0;
    for(int i=1;i<=n;i++){
        int id=lower_bound(ans,ans+k,a[i])-ans;
        if(id<k)ans[id]=a[i];
        else ans[k++]=a[i];
    }
    printf("%d\\n",k);
    return 0;
}

导弹防御系统

为了对抗附近恶意国家的威胁, R R R 国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。

给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式
输入包含多组测试用例。

对于每个测试用例,第一行包含整数 n n n,表示来袭导弹数量。

第二行包含 n n n 个不同的整数,表示每个导弹的高度。

当输入测试用例 n = 0 n=0 n=0 时,表示输入终止,且该用例无需处理。

输出格式
对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。

数据范围
1 ≤ n ≤ 50 1≤n≤50 1n50

输入样例:
5
3 5 2 4 1
0

输出样例:
2

样例解释
对于给出样例,最少需要两套防御系统。

一套击落高度为 3 , 4 3,4 3,4 的导弹,另一套击落高度为 5 , 2 , 1 5,2,1 5,2,1 的导弹。

分析: 这个题相比于上一个题,多了一个条件,也就是导弹拦截系统拦截的导弹既可以一直严格单调上升,也可以严格单调下降。而且 n n n最大也才 50 50 50。我们可以直接搜索,我们先考虑搜索顺序
搜索顺序分为两个阶段:

  • 从前往后枚举每颗导弹属于某个上升子序列,还是下降子序列;
  • 如果属于上升子序列,则枚举属于哪个上升子序列(包括新开一个上升子序列);如果属于下降子序列,可以类似处理。

u p [ k ] up[k] up[k] d o w n [ k ] down[k] down[k]记录第k套上升(下降)系统目前所拦截的最后一个导弹
d f s ( t , u , v ) dfs(t,u,v) dfs(t,u,v)意味着已有 u u u个上升, v v v个下降,正在处理第 t t t个数
放在上升/下降系统中的哪个位置和上一道题目所找的方法是一样的。
注意:

  • u p up up数组所存的是严格单调上升的导弹系统的最后一个数,上升导弹系统的最后一个数是非严格递减的
  • d o w n down down数组所存的是严格单调下降的导弹系统的最后一个数,下降导弹系统的最后一个数是非严格递增的

可以看出,我们的搜索空间好像挺大的,如果我们当前两种系统的数量已经大于等于最优值了,我们可以剪枝。其实这个题手写二分常数会小一些,跑的会快很多。或者直接顺序搜索第一个小于 x x x的数,第一个大于 x x x的数,对于这个题来说也会快一些(应该是数据的原因)。

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
int n,ans=105;
int a[55],up[55],down[55];
void dfs(int cnt,int up_num,int down_num){
    if(up_num+down_num>=ans)return ;
    if(cnt==n+1){
        ans=up_num+down_num;
        return ;
    }
    //将当前数放在严格单调上升的导弹系统中,上升导弹系统的最后一个数是非严格递减的
    int id=upper_bound(up,up+up_num,a[cnt],greater<int>())-up,temp;
    if(id<up_num)temp=up[id],up[id]=a[cnt],dfs(cnt+1,up_num,down_num),up[id]=temp;
    else up[up_num]=a[cnt],dfs(cnt+1,up_num+1,down_num);
    //将当前数放在严格单调下降的导弹系统中,下降导弹系统的最后一个数是非严格递增的
    id=upper_bound(down,down+down_num,a[cnt])-down;
    if(id<down_num)temp=down[id],down[id]=a[cnt],dfs(cnt+1,up_num,down_num),down[id]=temp;
    else down[down_num]=a[cnt],dfs(cnt+1,up_num,down_num+1);
}
int main()
{
    while(~scanf("%d",&n)){
        if(n==0)break;
        ans=105;
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        dfs(1,0,0);
        printf("%d\\n",ans);
    }
    return 0;
}

最长公共上升子序列

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列 A A A B B B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列 A A A B B B 的长度均不超过 3000 3000 3000

输入格式
第一行包含一个整数 N N N,表示数列 A A A B B B 的长度。

第二行包含 N N N 个整数,表示数列 A A A

第三行包含 N N N 个整数,表示数列 B B B

输出格式
输出一个整数,表示最长公共上升子序列的长度。

数据范围
1 ≤ N ≤ 3000 1≤N≤3000 1N3000,序列中的数字均不超过 2 31 − 1 2^{31}−1 2311

输入样例:
4
2 2 1 3
2 1 2 3

输出样例:
2

分析: 动态规划题,结合了LIS和LCS,在状态表示和状态计算上融合了两者
状态表示:

  • d p [ i ] [ j ] dp[i][j] dp[i][j]代表所有 a [ 1 − i ] a[1 - i] a[1i] b [ 1 − j ] b[1 - j] b[1j]中以 b [ j ] b[j] b[j]结尾的公共上升子序列的集合;
  • d p [ i ] [ j ] dp[i][j] dp[i][j]的值等于该集合的子序列中长度的最大值;

状态计算(对应集合划分):
首先依据公共子序列中是否包含 a [ i ] a[i] a[i],将 d p [ i ] [ j ] dp[i][j] dp[i][j]所代表的集合划分成两个不重不漏的子集

  • 不包含 a [ i ] a[i] a[i]的子集,最大值是 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j]
  • 包含 a [ i ] a[i] a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
  • 子序列只包含 b [ j ] b[j] b[j]一个数,长度是1;
  • 子序列的倒数第二个数是 b [ 1 ] b[1] b[1]的集合,最大长度是 d p [ i − 1 ] [ 1 ] + 1 dp[i - 1][1] + 1 dp[i1][1]+1
  • 子序列的倒数第二个数是 b [ j − 1 ] b[j - 1] b[j1]的集合,最大长度是 d p [ i − 1 ] [ j − 1 ] + 1 dp[i - 1][j - 1] + 1 dp[i1][j1]+1

如果直接按上述思路实现,需要三重循环:见下面代码中的第一个

这样肯定会超时,我们需要优化,每次循环中,我们要更新的 d p [ i ] [ j ] dp[i][j] d以上是关于最长上升子序列模型的主要内容,如果未能解决你的问题,请参考以下文章

动态规划之最长上升子序列模型

动态规划 最长上升子序列模型——进阶

动态规划 最长上升子序列模型——进阶

最长上升子序列模型

最长上升子序列模型

数字三角形模型和最长上升子序列模型 AcWing题目