1.2.2最长上升子序列模型(二)

文章讨论了导弹拦截系统的设计问题,引入了Dilworth定理来解决最长不升子序列问题,并展示了如何通过贪心法和动态规划求解。同时,文章提到了导弹防御系统升级的需求,指出了解决多个单调序列的策略,通过搜索和贪心思想确定所需防御系统数量。最后,提到了最长公共上升子序列的计算,结合了最长上升子序列和最长公共子序列的解题思路。
摘要由CSDN通过智能技术生成

1.拦截导弹

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

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

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

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

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

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

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

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

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

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2
题解
Dilworth 定理

狄尔沃斯定理(Dilworth’s theorem)亦称偏序集分解定理,该定理断言:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真,它断言:对于任意有限偏序集,其最长链中元素的数目必等于其最小反链划分中反链的数目

该定理在子序列问题上可表述为:把序列分成不升子序列的最少个数,等于序列的最长上升子序列长度。把序列分成不降子序列的最少个数,等于序列的最长下降子序列长度

该定理在二分图上等价于柯尼希定理:二分图最小点覆盖的点数等于最大匹配数

定理法
给定正整数序列,求最长不升子序列长度,以及能覆盖整个序列的不升子序列的最少个数

问题一套用 LIS模型即可求解。由 Dilworth 定理,问题二等价于 LIS长度

#include <cstdio>

#define max(a, b) ((a) > (b) ? (a) : (b))
#define rep(i, s, e) for (int i = s; i <= e; i ++)
#define N 1010

int a[N], f[N], g[N];

int main() {
    int n = 1; while (~scanf("%d", a + n)) n ++; n --;

    rep(i, 1, n) rep(j, 0, i - 1) {
        bool down = a[j] >= a[i];
        f[i] = max(f[i], down * f[j] + 1);
        g[i] = max(g[i], !down * g[j] + 1);
    }

    int r1 = 0, r2 = 0;
    rep(i, 1, n) r1 = max(r1, f[i]), r2 = max(r2, g[i]);

    printf("%d\n%d\n", r1, r2);

    return 0;
}
贪心法

在这里插入图片描述
对于每个数,既可以把它接到已有子序列后面,也可以建立一个新序列。要使子序列数最少,应尽量不建立新序列。此外,应让每个子序列的末尾尽可能大,这样能接的数更多。因为一个数若能接到小数后面,必然能接到大数后面,反之则不成立。根据这些想法,可总结出如下贪心流程:

从前往后扫描每个数,对于当前数

  1. 若现有子序列的结尾都小于它,则创建新子序列
  2. 否则,将它放到结尾大于等于它的最小数后面

证明

A A A为贪心解, B B B为最优解

  1. 贪心解能覆盖所有数,且形成的都是不升序列,因此合法。由定义, B ≤ A B≤A BA
  2. 假设最优解对应的方案和贪心方案不同,从前往后找到第一个不在同一序列的数 x x x。假设贪心解中 x x x前面的数是 a a a,最优解中 x x x 前面的数是 b b b a a a 后面的数是 y y y,由于贪心会让当前数接到大于等于它的最小数后面,所以 x , y ≤ a ≤ b x,y≤a≤b x,yab,此时,在最优解中,把 x x x 一直到序列末尾,和 y y y 一直到序列末尾交换位置,这样做不影响正确性,也不增加序列个数,但会使 x x x 在最优解和贪心解中所处的位置相同。由于序列中的数是有限的,只要一直做下去,一定能使最优解变为贪心解。因此 A ≤ B A≤B AB

综上 A = B A=B A=B

实现
g g g保存每条不升子序列的末尾,可用归纳法证明 g g g是单调上升的。初始时, g g g为空满足条件。假设 g g g已经单调上升,现在要加入数 x x x,设 g [ i ] g[i] g[i]是大于等于 x x x的最小数,则 g [ i − 1 ] < x ≤ g [ i ] ≤ g [ i + 1 ] g[i−1]<x≤g[i]≤g[i+1] g[i1]<xg[i]g[i+1]。由于 x x x要放到 g [ i ] g[i] g[i] 后面,因此 g [ i ] g[i] g[i] 会被更新成 x x x,之后满足 g [ i − 1 ] < g [ i ] ≤ g [ i + 1 ] g[i−1]<g[i]≤g[i+1] g[i1]<g[i]g[i+1],其它元素不受影响,整个序列仍单调上升。由此得证

可发现上述贪心实现,和最长上升子序列的贪心解法代码相同,这也印证了 Dilworth 定理

#include <cstdio>

#define max(a, b) ((a) > (b) ? (a) : (b))
#define rep(i, s, e) for (int i = s; i <= e; i ++)
#define N 1010

int a[N], f[N], g[N];

int main() {
    int n = 1; while (~scanf("%d", a + n)) n ++; n --;

    rep(i, 1, n) rep(j, 0, i - 1)
        f[i] = max(f[i], (a[j] >= a[i]) * f[j] + 1);

    int r = 0; rep(i, 1, n) r = max(r, f[i]);
    printf("%d\n", r);

    r = 0;
    rep(i, 1, n) {
        int j = 0; while (j < r && g[j] < a[i]) j ++;
        g[j] = a[i]; r += j == r;
    }
    printf("%d\n", r);

    return 0;
}

y总做法

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n;
int q[N];
int f[N],g[N];
int main()
{
    while(cin >> q[n]) n++;
    
    int res = 0;
    for(int i = 0;i < n;i++)
    {
        f[i] = 1;
        for(int j = 0;j < i;j++)
            if(q[j] >= q[i])
                f[i] = max(f[i],f[j] + 1);
        res = max(res,f[i]);
    }
    cout<<res<<endl;
    
    //第二问
    int cnt = 0;//表示当前子序列的个数
    for(int i = 0;i < n;i++)
    {
        int k = 0;//k是从前往后找的序列
        while(k < cnt && g[k] < q[i]) k++;
        g[k] = q[i];
        if(k >= cnt)cnt++;
    }
    cout<<cnt<<endl;
    
    return 0;
}

2.导弹防御系统

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

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

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

题解

  • 概述

该题也是一个典型的最长上升子序列的问题。

条件: 导弹拦截高度要么一直上升要么一直下降。

有的导弹可以选择上升,有的可以选择下降,不是单纯地问所存在的序列可以划分为多少组上升子序列的问题,所以不能用之前的方法解。

怎么做?
当找不到办法时,考虑使用枚举法

如何做?
从问题的解出发,最终问题的答案是有许多单调上升子序列和许多单调下降子序列,那么实际就是对于每个数,来思考将该数放到上升序列中还是下降序列中。

  • 题解

注意顺序性
  • 依次枚举每个数
    先枚举将该数作为单调上升的序列,还是单调下降的序列中

    • 如果该数被放到了单调上升的序列中,则枚举将该数放到哪个单调上升的序列后面
    • 如果该数被放到了单调下降的序列中,则枚举将该数放到哪个单调下降的序列后面。
    • 或者该数成为一个单独的单调序列

    实际上来看这就是一个建模问题,首先建模为初始状态,初始状态可以经过很多离散步骤到达一个新的状态,最终不断地到达目的状态。

暴力,考虑搜索空间,首先如果搜索的序列是有序的,那么就可以使用二分法。每一步搜索空间是50,那么这样下去,就是5050…, 最终是指数级别

up[] 存储当前所有上升子序列的末尾元素
down[] 存储当前所有下降子序列的末尾元素

所以这里实际上用到了上一题导弹防御的结果来做,对于多个单调上升(下降)子序列只需要对末尾元素进行考虑即可。
这一点真厉害!!!
然后这里又可以进行优化

使用贪心的思想
x x x 尽可能覆盖一个末尾元素大的序列

因此是一个很强的优化,所以每一步实际上只有两种选择,要么上升,要么下降。

up本身是单调的,所以一方面找到即停止,另一方面可以直接考虑用二分

如何进行搜素

  1. 使用BFS, BFS是宽度优先,需要进行存储(空间太多,不太好剪枝),而且代码写起来比较麻烦
  2. 使用DFS, 一种想法是使用全局变量,第二种方式是迭代加深

代码实现

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 55;

int n;
int q[N];
int up[N],down[N];//一个表示上升子序列结尾,另外一个表示下降子序列结尾
int ans;

//su表示当前上升子序列个数,sd表示当前下降子序列个数
void dfs(int u,int su,int sd)
{
    if(su + sd >= ans) return;
    if(u == n)
    {
        ans = su + sd;
        return;
    }
    //情况1:将当前数放到上升子序列中
    int k = 0;
    while(k <= su && up[k] >= q[u]) k++;
    int t = up[k];
    up[k] = q[u];
    if(k < su)  dfs(u + 1,su,sd);
    else dfs(u + 1,su + 1,sd);
    up[k] = t;
    
    //情况2:将当前数放到下降子序列中
    k = 0;
    while(k < sd && down[k] <= q[u])    k++;
    t = down[k];
    down[k] = q[u];
    if(k < sd)  dfs(u + 1,su,sd);
    else dfs(u + 1,su,sd + 1);
    down[k] = t;
}
int main()
{
    while(cin >> n,n)
    {
        for(int i = 0;i < n;i++)    cin >> q[i];
        
        ans = n;
        
        dfs(0,0,0);
        
        cout << ans << endl;
    }
    return 0;
}

3.最长公共上升子序列

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

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

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

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

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

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

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

第二行包含 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

题解(此题不是很懂,比较综合)在这里插入图片描述

(DP,线性DP,前缀和) O ( n 2 ) O(n^{2}) O(n2)

这道题目是AcWing 895. 最长上升子序列和AcWing 897. 最长公共子序列的结合版,在状态表示和状态计算上都是融合了这两道题目的方法。此两道题在之前博客均有叙述,同时可以在Acwing官网查看(算法提高课)

状态表示:

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

状态计算(对应集合划分):

首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:

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

如果直接按上述思路实现,需要三重循环:

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= n; j ++ )
    {
        f[i][j] = f[i - 1][j];
        if (a[i] == b[j])
        {
            int maxv = 1;
            for (int k = 1; k < j; k ++ )
                if (a[i] > b[k])
                    maxv = max(maxv, f[i - 1][k] + 1);
            f[i][j] = max(f[i][j], maxv);
        }
    }
}

然后我们发现每次循环求得的maxv是满足a[i] > b[k]f[i - 1][k] + 1的前缀最大值。
因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。

最终答案枚举子序列结尾取最大值即可。

C++ 代码

#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 3010;

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

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);

    for (int i = 1; i <= n; i ++ )
    {
        int maxv = 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], maxv);
            if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
        }
    }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
    printf("%d\n", res);

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Next---YOLO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值