第一章 动态规划 最长上升子序列模型

1、最长上升子序列模型

最基本的该模型用来求一个序列中,上升子序列的最长值(例题1)。这个基本问题有两种处理方式,一种是使用动态规划来进行处理,时间复杂度为 O ( n 2 ) O(n^2) O(n2),另一种使用贪心来做,时间复杂度为 O ( n l o g N ) O(nlogN) O(nlogN)。此后所有引申出来的题目在求最长上升子序列的时候,都可以用两种方式来求。

由这个基础问题可以引申出求一个序列中最长的上升或下降子序列的最长值的问题(例题2),进而可以引申出求一个序列中的“山峰”形子序列的最长值(例题3)。

也可以引申出例题4,这个题本质就是最基础的上升子序列的最长值,但是从原问题转化成该问题比较困难,这告诉我们看问题要看本质,分析出题目的性质之后进行问题的转换,转换成我们已经解决的问题。

也可以引申出求一个序列中和最大的上升子序列(例题5),需要对求动态规划的过程进行一些改变。

该模型与贪心结合,可以引申出求最少用多少下降序列可以将所给的序列覆盖的问题(例题6)。该问题的解决实际上和最基本模型的贪心做法相一致,这两个问题是对偶问题。再与DFS结合,可以用来处理最少用多少上升或者下降序列可以将所给的序列覆盖,这里可以选择使用上升或者下降序列进行覆盖,所以要结合dfs进行暴搜+剪枝(例题7)。

该模型也可以和最长公共子序列结合,求最长公共上升子序列(例题8)。

2、例题

1. 最长上升子序列

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1000,
−10^9≤数列中的数≤10^9
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

状态表示f[i],表示从0到i的所有上升子序列最大值
集合划分
对于数组a,假设当前节点为a[i]

  1. 本节点的状态是从a[1]过来的(也就是这个上升子序列中,a[i]的上一个节点的值是a[1])
  2. 本节点的状态是从a[2]过来的
  3. 本节点的状态是从a[3]过来的
  4. 本节点的状态是从a[k]过来的

状态计算
对于某个集合划分a[k]来说,必然是这个样的情况

第k类中的上升子序列最后一个是a[i],倒数第二个是a[k](也就是a[i]这个状态是从a[k]来的),
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]

max(所有_,_,_,…,_,_,a[k]) + 1(也就是加上a[i]自己)就是从a[k]走到a[i]的这个序列的最大值。
而max(所有_,_,_,…,_,_,a[k])按照我们的定义就是f[k],就是从0到k的所有上升子序列的长度的最大值
那么就有从0经过a[k]直接到a[i]的这个序列的最大值等于f[k] + 1
而f[i]=max(f[1],f[2],…f[k]) + 1;

状态表示主要看设问是怎么设问的,而集合划分主要看当前状态可以如何获得。
第一种做法

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010;
int q[N],f[N];
int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        cin >> q[i];
    }
    
    int res = -2e9;
    //初始化为1,是因为一个上升子序列的元素个数最少就是1
    for(int i = 0; i < n; i ++) f[i] = 1;
    for(int i = 0; i < n; i ++)
    {
        for(int j = 0; j < i; j ++)
        {
            if(q[i] > q[j])
            {
                f[i] = max(f[i],f[j] + 1);
            }
        }
    }
    for(int i = 0; i < n; i ++)
    {
        res = max(res,f[i]);   
    }
    cout << res;
}

第二种做法
使用贪心的做法,具体的贪心策略是:
设当前元素为a[i],结果数组为s

  • 当数组中没有比a[i]更大的元素的时候,将a[i]放入s
  • 否则,寻找数组中第一个大于等于a[i]的元素,将它替换为a[i]

最后s中的元素数量就是最长的上升子序列的数量

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5  + 10;
int s[N],tt = 0;
int main()
{
    int n,t;
    cin >> n;
    cin >> t;
    s[tt] = t;
    int res = -1;
    for(int i = 0; i < n - 1; i ++)
    {
        cin >> t;
        //当s中最大的数字也比t小的话,就把t放在s的最后,这样操作之后,s中的数字是单调上升的
        if(s[tt] < t) s[++ tt] = t;
        else{
            //因为s是单调的,所以可以使用二分找到s中大于等于当前t的第一个数字
            int l = 0,r = tt;
            while(l < r)
            {
                int mid = (l + r) >> 1;
                if(s[mid] < t)
                {
                    l = mid + 1;
                }else{
                    r = mid;
                }
            }
            //将这个大于等于t的第一个数字换成t,因为被替换掉的是比当前数大的数字,替换之后,整个还是单调的队列
            s[l] = t;
        }
    }
    cout << tt + 1;
}

2. 怪盗基德的滑翔翼

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。
不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。
初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。
因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式
输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式
对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围
1≤K≤100,
1≤N≤100,
0<h<10000
输入样例:
3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10
输出样例:
6
6
9

这个题其实是问这个序列中,所有可能的上升子序列和下降子序列中的最大值。
想法其实很简单,对每个节点,计算以它为终点的最长上升子序列的值,和以它为起点的最长下降子序列的值。在所有的值中,找到最大值。
而最长下降子序列可以看成从右往左的这个序列的最长上升子序列。

#include<iostream>
#include<algorithm>
using namespace std;
int q[110],f[110];
int main()
{
    int k;
    cin >> k;
    while(k --)
    {
        int n;
        cin >> n;
        for(int i = 0; i < n; i ++)
        {
            cin >> q[i];
            f[i] = 1;
        }
        int res = -1;
        for(int i = 0; i < n; i ++)
        {
            for(int j = 0; j < i; j ++)
            {
                if(q[i] > q[j])
                {
                    f[i] = max(f[i],f[j] + 1);
                }
            }
            res = max(f[i],res);
        }
        for(int i = 0; i < n; i ++)
        {
            f[i] = 1;
        }
        //从右往左遍历,找到“最长下降子序列”
        for(int i = n - 1; i >= 0; i --)
        {
            for(int j = n - 1; j > i; j --)
            {
                if(q[i] > q[j])
                {
                    f[i] = max(f[i],f[j] + 1);
                }
            }
            res = max(f[i],res);
        }
        cout << res << endl;
    }
}

3.登山

五一到了,ACM队组织大家去登山观光,队员们发现山上一共有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

输入格式
第一行包含整数N,表示景点数量。
第二行包含N个整数,表示每个景点的海拔。

输出格式
输出一个整数,表示最多能浏览的景点数。

数据范围
2≤N≤1000
输入样例:
8
186 186 150 200 160 130 197 220
输出样例:
4

总和来看题目中有三个条件

  1. 必须按照编号递增的顺序来进行浏览
  2. 相邻的两个景点不能相同
  3. 一旦开始下降了,就不能上
    在这里插入图片描述

实际上是求类似这个折线样子的最大距离
想法其实很简单,对每个节点,计算以它为终点的最长上升子序列的值,和以它为起点的最长下降子序列的值。对于所有节点,找出这两者中的最大值。可以看到,上升和下降两个序列的值都是独立的,所以可以分别求以a[i]结尾的最长上升子序列,和以a[i]结尾的最厂下降子序列,因此可以先预处理两边的再去找最大值

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N],q[N],g[N];
int main()
{
   int n;
   cin >> n;
   for(int i = 0; i < n; i ++)
   {
       cin >> q[i];
       f[i] = 1,g[i] = 1;
   }
   for(int i = 0; i < n; i ++)
   {
       for(int j = 0; j < i; j ++)
       {
           if(q[i] > q[j])
           f[i] = max(f[i],f[j] + 1);
       }
   }
   int res = -1;
   for(int i = n - 1; i >= 0; i --)
   {
       for(int j = n - 1; j > i; j --)
       {
           if(q[i] > q[j])
           g[i] = max(g[i],g[j] + 1);
       }
       // -1是因为f[i]和g[i]都包含i这个节点,多了一个
       res = max(g[i] + f[i] - 1,res);
   }
   cout << res;
}

4. 友好城市

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入格式
第1行,一个整数N,表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式
仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围
1≤N≤5000,
0≤xi≤10000
输入样例:
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
输出样例:
4

在这里插入图片描述
上图代表每个城市与和对岸的友好城市
题目要求有两个条件:

  1. 每个城市上只能建立一座桥
  2. 每座桥之间不能交叉
    也就是选择尽量多的不相交的连线

其本质就是所有选出来的两边的节点都是单调增加的,例如
在这里插入图片描述
所以策略就是按照某一岸的序号单增排序,找另一岸的序列的最长上升子序列就可以了。这样可以保证找到最多的不交叉的连线。

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入格式
第1行,一个整数N,表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式
仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围
1≤N≤5000,
0≤xi≤10000
输入样例:
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
输出样例:
4
#include<vector>
#include<algorithm>
#include<iostream>
using namespace std;
typedef pair<int,int> PII;
vector<PII> q;
int f[5010];

int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        int a,b;
        cin >> a >> b;
        q.push_back({a,b});
    }
    sort(q.begin(),q.end());
    int res = -1;
    for(int i = 0; i < n; i ++)
    {
        f[i] = 1;
        for(int j = 0; j < i; j ++)
        {
            if(q[i].second > q[j].second)
            f[i] = max(f[i],f[j] + 1);
        }
        res = max(res,f[i]);
    }
    cout << res;
}

5. 最大上升子序列的和

一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。
对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1<i2<…<iK≤N。
比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。
这些子序列中和最大为18,为子序列(1,3,5,9)的和。
你的任务,就是对于给定的序列,求出最大上升子序列和。
注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入格式
输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出格式
输出一个整数,表示最大上升子序列和。

数据范围
1≤N≤1000
输入样例:
7
1 7 3 5 9 4 8
输出样例:
18

状态表示f[i],表示从0到i的所有上升子序列和的最大值
集合划分
对于数组a,假设当前节点为a[i]

  1. 本节点的状态是从a[1]过来的(也就是这个上升子序列中,a[i]上一个节点的值是a[1])
  2. 本节点的状态是从a[2]过来的
  3. 本节点的状态是从a[3]过来的
  4. 本节点的状态是从a[k]过来的

状态计算
对于某个集合划分a[k]来说,必然是这个样的情况

第k类中的上升子序列最后一个是a[i],倒数第二个是a[k](也就是a[i]这个状态是从a[k]来的),
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]
_,_,_,...,_,_,a[k],a[i]

max(所有_,_,_,…,_,_,a[k]) + a[i](也就是加上a[i]自己的值)就是从a[k]走到a[i]的这个序列的和的最大值。
而max(所有_,_,_,…,_,_,a[k])按照我们的定义就是f[k],就是从0到k的所有上升子序列的和的最大值
那么就有从0经过a[k]直接到a[i]的这个序列的最大值等于f[k] + a[i]
则f[i]=max(f[1],f[2],…f[k]) + a[i]

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N],q[N];
int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n; i ++)
    {
        cin >> q[i];
    }
    int res = -1;
    for(int i = 0; i < n; i ++)
    {
        f[i] = q[i];
        for(int j = 0; j < i; j ++)
        {
            if(q[i] > q[j])
            {
                f[i] = max(f[i],f[j] + q[i]);
            }
        }
        res = max(res,f[i]);
    }
    cout << res;
}

6.拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

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

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

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

输入样例:
389 207 155 300 299 170 158 65
输出样例:
6
2

第一问之前说过。只看第二问,其本质是问最少用多少下降序列可以将所给的序列覆盖的问题。该问题的解决实际上和最基本模型的贪心做法相一致,这两个问题是对偶问题。

我们贪心的流程是:
从前向后扫描每个数字,对于每个数字

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

证明:
A表示贪心得到的序列个数。B代表最优序列个数
易得B <= A
下证A <= B
从后往前看A,找到第一个与B的序列不同的元素x。
由我们的贪心条件可知,a是所有大于等于x的元素的最小值,所以a <= b,则x也可以放在b的后面。这样可以把A的每个序列改造成B, 则A <= B
在这里插入图片描述
这里我们可以把每个序列的最后一个元素存入一个数组g中。根据贪心条件,这个g数组应该是单调上升的。因为每次增加数组是因为没有任何一个组的最后一个元素的值大于当前元素。而寻找“结尾大于等于它的最小的子序列”就变成了在这个g数组中,寻找大于等于该元素的第一个元素。我们可以将其替换成当前元素,也就相当于放到这个序列的末尾了。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 1010;
vector<int> q;
int f[N],g[N];
int main()
{
    int n;
    while(cin >> n)
    {
      q.push_back(n);   
    }
    
    int m = -1;
    for(int i = q.size() - 1; i >= 0; i --)
    {
        f[i] = 1;
        for(int j = q.size() - 1;j > i; j --)
        {
            if(q[i] >= q[j])
            {
                f[i] = max(f[i],f[j] + 1);
            }
        }
        m = max(m,f[i]);
    }
    cout << m << endl;
    
    int res = -1;
    g[++ res] = q[0];
    for(int i = 0; i < q.size(); i ++)
    {
        int t = q[i];
        if(g[res] < t){
            g[++ res] = t;
            continue;
        }
        int l = 0, r = res,mid;
        while(l < r)
        {
            mid = (l + r) >> 1;
            if(g[mid] <= t)
            {
                l = mid + 1;
            }else{
                r = mid;
            }
        }
        g[l] = t;
    }
    cout << res + 1;
}

代码写出来跟例题1的做法2一样。一个序列最少用多少非上升子序列将它覆盖掉,是等于最长上升子序列的方案数的,Dilworth定理系统描述了这种问题。

7. 导弹防御系统

为了对抗附近恶意国家的威胁,R 国更新了他们的导弹防御系统。
一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式
输入包含多组测试用例。
对于每个测试用例,第一行包含整数 n,表示来袭导弹数量。
第二行包含 n 个不同的整数,表示每个导弹的高度。
当输入测试用例 n=0 时,表示输入终止,且该用例无需处理。

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

数据范围
1≤n≤50
输入样例:
5
3 5 2 4 1
0 
输出样例:
2
样例解释
对于给出样例,最少需要两套防御系统。

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

这个题是LIS(最长上升子序列问题) + 贪心 + DFS的结合。
其本质是最少可以用多个上升和下降子序列将一个序列覆盖掉。此题可以用上升的序列也可以用下降的序列进行覆盖,所以之前的贪心不能再使用,要更换贪心流程。
这里我们的贪心流程如下:
从前向后扫描每个数字,对于每个数,都有两种选择,要么用上升序列覆盖,要么用下降序列覆盖。
对于用上升序列覆盖的情形:

  1. 如果现在的子序列的结尾都小于当前数,则创建新子序列
  2. 将当前数放到结尾大于等于它的最小的子序列的后面

对于用下降序列覆盖的情形:

  1. 如果现在有的子序列的结尾都小于当前数,则创建新的子序列
  2. 将当前数放到结尾大于等于它的最小的子序列后面
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
vector<int> q;
int n;
//fup是上升序列的所有元素的结尾,fdown是下降序列的所有元素的结尾
int fup[55],fdown[55];
//i代表当前元素的位置,up表示上升序列中元素数量,down表示下降序列中元素的数量
//注意:上升序列的结尾是严格下降的,下降序列的各个组的结尾序列是上升的
void dfs(int i, int up, int down)
{
   //如果up + down大于等于ans的话,则ans不会在接下来的计算中变得更小,所以可以直接返回
   if(up + down >= ans) return;
   //i == n代表遍历到了最后一个
   if(i == n)
   {
   	   //此时结果一定是最小值,因为不是最小值的已经在上面返回了
       ans = up + down;
       return;
   }
   //上升序列的情形1
   if(!up || fup[up] > q[i]){
       fup[up + 1] = q[i];
       dfs(i + 1, up + 1, down);
   }else{
   //上升序列的情形2
       int l = 1, r = up,mid;
       int t = q[i];
       //二分找小于等于的第一个数字
       while(l < r)
       {
           mid = (l + r) >> 1;
           if(fup[mid] >= t)
           {
              l = mid + 1;
           }else{
              r = mid;
           }
        }
       int tmp = fup[l];
       fup[l] = t;
       dfs(i + 1, up, down);
       fup[l] = tmp;
   }
   
    //下降序列的情形1
    if(!down || fdown[down] < q[i])
    {
        fdown[down + 1] = q[i];
        dfs(i + 1, up, down + 1);
    }else{
    //下降序列的情形2
        int l = 1, r = down,mid;
        int t = q[i];
        //二分找大于等于的第一个数字
        while(l < r)
       {
          mid = (l + r) >> 1;
          if(fdown[mid] <= t)
          {
              l = mid + 1;
          }else{
              r = mid;
          }
        }
   		int tmp = fdown[l];
    	fdown[l] = t;
    	dfs(i + 1, up, down);
    	fdown[l] = tmp;
 	}
}
int main()
{
    while(cin >> n,n)
    {
        q.clear();
        for(int i = 0; i < n; i ++)
        {
          int t;
          cin >> t;
          q.push_back(t);
        }
        ans = 101;
        dfs(0,0,0);
        cout << ans << endl;
    }
    
}

8.最长公共子序列

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列 A 和 B 的长度均不超过 3000。

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

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

数据范围
1≤N≤3000,序列中的数字均不超过 231−1。

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

状态表示 f[i,j],表示所有第一个序列的前i个字母和第二个序列的前j个字母构成,并且以b[j]结尾的公共上升子序列的长度最大值
集合划分
首先按照a[i]是否在公共上升序列中分成两类:

  1. 表示所有不包含a[i]的公共上升子序列
    按照定义,可以表示成f[i-1,j]
  2. 表示所有包含a[i]的公共上升子序列(因为以b[j]结尾,所以此时a[i]一定等于b[j])
    直接计算不好计算,则将这一类集合按照公共子序列的前一个元素是什么进行再次划分
    对于数组a,假设当前节点为a[i]
    1 本节点的状态是从b[1]过来的(也就是这个上升子序列中,a[i]的上一个节点的值是a[1])
    2 本节点的状态是从b[2]过来的
    3 本节点的状态是从b[3]过来的
    4 …
    5 本节点的状态是从b[k]过来的

在这里插入图片描述
状态计算

  1. 右边的计算很简单,就是f[i - 1,j]
  2. 而左边需要分析一下。首先左边集合表示所有第一个序列的前i个字母和第二个序列的前j个字母构成,并且以b[j]结尾的并且一定包含a[i]的公共上升子序列的长度最大值。此时a[i] = b[j],放大来看每一个小集合b[k],其实代表的是这样一种情况
第k类中的上升子序列最后一个是a[i],倒数第二个是b[k](也就是a[i]这个状态是从b[k]来的),所有可能的序列假如可以列举如下。注意,这里b[k] < a[i](b[j])(因为要上升序列)
_,_,_,...,_,_,b[k],a[i]
_,_,_,...,_,_,b[k],a[i]
_,_,_,...,_,_,b[k],a[i]
_,_,_,...,_,_,b[k],a[i]

我们可以看到max(所有_,_,_,…,_,_,b[k]的长度)正是f[i - 1,k]。因为最后一个元素是a[i],所以公共子序列前面的元素中不含有a[i],而且以b[k]结尾。
所以左边的元素可以表示为max(f[i - 1,1],f[i - 1,2],…f[i - 1,k]) + 1

//未优化版本
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 3010;
int a[N],b[N],f[N][N];
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= n; i ++) cin >> b[i];
    
    //f[i,j] 代表从0到a[i],以及0到b[j]且以b[j]结尾的公共单调上升子序列
    //a[i]不在公共单调上升子序列中
    for(int i = 1; i <= n; i ++)
    {
        for(int j = 1; j <= n; j ++)
        {
            f[i][j] = max(f[i][j],f[i - 1][j]);
            if(a[i] == b[j])
            {
                //从0开始遍历是因为有“空”这个集合
                for(int k = 0; k < j; k ++)
                {
                   if(b[k] < b[j]) f[i][j]  = max(f[i - 1][k] + 1, f[i][j]); 
                }
            }
        }
    }
    
    int res = 0;
    //第二个序列以哪个数字结尾
    for(int i = 1; i <= n; i ++) res = max(res, f[n][i]);
    cout << res;
}

注意到代码的这一段,

if(a[i] == b[j])
{
 	//从0开始遍历是因为有“空”这个集合
	for(int k = 0; k < j; k ++)
	{
		if(b[k] < b[j]) f[i][j]  = max(f[i - 1][k] + 1, f[i][j]); 
	}
}

由于b[j] = a[i]所以可以改写成

if(a[i] == b[j])
{
 	//从0开始遍历是因为有“空”这个集合
	for(int k = 0; k < j; k ++)
	{
		if(b[k] < a[i]) f[i][j]  = max(f[i - 1][k] + 1, f[i][j]); 
	}
}

这段试求从0到j的f[i - 1][k]的最大值,可以将它等价变形为下面的样子

//通过代码等价变形得到
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 3010;
int a[N],b[N],f[N][N];
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i <= n; i ++) cin >> b[i];
    
    //f[i,j] 代表从0到a[i],以及0到b[j]且以b[j]结尾的公共单调上升子序列
    //a[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(f[i - 1][j] + 1,maxv);
        }
    }
    
    int res = 0;
    //第二个序列以哪个数字结尾
    for(int i = 1; i <= n; i ++) res = max(res, f[n][i]);
    cout << res;
}

3、参考资料

Acwing算法基础课以及算法提高课
Dilworth定理

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值