You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

LIS+LCS专题

基础题

AcWing 895. 最长上升子序列

O(N^2)算法

状态表示 f[i]:以a[i]结尾的最长上升子序列长度

状态转移

    for (int i = 1; i <= n; i++) {
        f[i] = 1; //只有a[i]一个数
        for (int j = 1; j < i; j++)
            if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
    }

答案

for (int i = 1; i <= n; i++) res = max(res, f[i]);

AcWing 896. 最长上升子序列 II

数据量增大:N<=100000,O(LogN*N)算法

状态表示 f[i]表示长度为i的递增子序列中,末尾元素最小的是f[i]

答案 fl

状态转移f[]数组是一个单独上升的数组,这是可以二分的基础 ② 每个长度都争取替换掉前面记录数组中第一个大于等于自己的数字,以保证长度不变的情况下,数字最小,因为只有最小才能让后面的其它数字更容易接上去,机会才能更多。

 // 1、首个元素入栈
f[0] = a[0];

// 2、后续元素开始计算
for (int i = 1; i < n; i++) {
    if (a[i] > f[fl])
        f[++fl] = a[i];
    else
        //利用STL的二分在f中查找第一个大于等于a[i]的值,并完成替换
        *lower_bound(f, f + fl, a[i]) = a[i];
}    

也可以这样写

memset(f,-0x3f,sizeof f);
   
// 2、后续元素开始计算
for (int i = 1; i <= n; i++) {
    if (a[i] > f[fl])
        f[++fl] = a[i];
    else
        //利用STL的二分在f中查找第一个大于等于a[i]的值,并完成替换
        *lower_bound(f + 1, f + 1 + fl, a[i]) = a[i];
}

AcWing 897. 最长公共子序列

状态表示 定义f[i][j]a[]i结尾,b[]j结尾的最长公共子序列长度

说明:没有说a[i]或者b[j]一定要出现在最长公共子序列当中!这个最长公共子序列,可能是a[]b[]的一些前序组成的,a[i],b[j]也可能没有对结果产生贡献。

  • a[i]==b[j]时,看一下两个字符串的前序,发现在少了a[i],b[j]后,转化为子问题f[i-1][j-1],问题转化为$f[i][j]=f[i-1][j-1]+1$

  • a[i] \neq b[j]时:

    • 如果a[i]不产生贡献,那么把它干掉f[i-1][j]
    • 如果b[j]不产生贡献,那么把它干掉f[i][j-1]
for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            if (a[i] == b[j])
                f[i][j] = f[i - 1][j - 1] + 1;
            else
                f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        }

答案 f[n][m]

进阶题

AcWing 1017. 怪盗基德的滑翔翼

朴素O(N^2)版本

int mx = 0;
//从左到右,求一遍最长上升子序列
for (int i = 1; i <= n; i++) {
    f[i] = 1;
    for (int j = 1; j < i; j++)
        if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);
    mx = max(mx, f[i]);
}
//反向求解 LIS问题
for (int i = n; i >= 1; i--) {
    f[i] = 1;
    for (int j = n; j > i; j--)
        if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);
    mx = max(mx, f[i]);
}

优化O(LogN*N)版本

// 正着求
f[0] = a[0];
for (int i = 1; i < n; i++) {
    if (a[i] > f[fl])
        f[++fl] = a[i];
    else
        *lower_bound(f, f + fl, a[i]) = a[i];
}

// 反着求
g[0] = a[n-1];
for (int i = n - 1; i >= 0; i--) {
    if (a[i] > g[gl])
        g[++gl] = a[i];
    else
        *lower_bound(g, g + gl, a[i]) = a[i];
}
// pk的最大结果
res = max(fl, gl);
// 输出
printf("%d\n", res + 1);

AcWing 1014. 登山

//正向求解 LIS问题
for (int i = 1; i <= n; i++) {
    f[i] = 1;
    for (int j = 1; j < i; j++)
        if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
}
//反向求解 LIS问题
for (int i = n; i >= 1; i--) {
    g[i] = 1;
    for (int j = n; j > i; j--)
        if (a[i] > a[j]) g[i] = max(g[i], g[j] + 1);
}
int res = 0;
//因为最终的那个中间点左边计算了一次右边双计算了一次需要减1去重复
for (int i = 1; i <= n; i++) res = max(res, f[i] + g[i] - 1);

AcWing 482. 合唱队形

//正向求解 LIS问题
for (int i = 1; i <= n; i++) {
    f[i] = 1;
    for (int j = 1; j < i; j++)
        if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
}
//反向求解 LIS问题
for (int i = n; i >= 1; i--) {
    g[i] = 1;
    for (int j = n; j > i; j--)
        if (a[i] > a[j]) g[i] = max(g[i], g[j] + 1);
}
int res = 0;
//因为最终的那个中间点左边计算了一次右边双计算了一次需要减1去重复
for (int i = 1; i <= n; i++) res = max(res, f[i] + g[i] - 1);
//输出
printf("%d\n", n-res);
//正向
f[++fl] = a[1];
p1[1] = 1;

for (int i = 2; i <= n; i++)
    if (a[i] > f[fl]) {
        f[++fl] = a[i];
        p1[i] = fl;
    } else {
        int t = lower_bound(f + 1, f + fl + 1, a[i]) - f;
        f[t] = a[i];
        p1[i] = t;
    }

//反向
g[++gl] = a[n];
p2[n] = 1;

for (int i = n - 1; i >= 1; i--)
    if (a[i] > g[gl]) {
        g[++gl] = a[i];
        p2[i] = gl;
    } else {
        int t = lower_bound(g + 1, g + gl + 1, a[i]) - g;
        g[t] = a[i];
        p2[i] = t;
    }

for (int i = 1; i <= n; i++) res = max(res, p2[i] + p1[i] - 1);

AcWing 1012. 友好城市 ① 数对 ② 左端点排序 ③ 对于右端点组成的数组,求最长上升子序列长度

for (int i = 1; i <= n; i++)cin >> q[i].first >> q[i].second;
    sort(q + 1, q + 1 + n);//按first排序
    int res = 0;
    for (int i = 1; i <= n; i++) {
        f[i] = 1;
        for (int j = 1; j < i; j++)
            if (q[i].second > q[j].second)
                f[i] = max(f[i], f[j] + 1);
        res = max(res, f[i]);
    }
    printf("%d\n", res);
typedef pair<int, int> PII;
#define x first
#define y second
PII a[N];     // 南岸和北岸的一对友好城市的坐标
...
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
    sort(a + 1, a + 1 + n);

    f[++fl] = a[1].y;
    for (int i = 2; i <= n; i++) {
        if (a[i].y > f[fl])
            f[++fl] = a[i].y;
        else
            *lower_bound(f + 1, f + 1 + fl, a[i].y) = a[i].y;
    }

    printf("%d\n", fl);

AcWing 1016. 最大上升子序列和

#include <bits/stdc++.h>
using namespace std;

// 最大上升子序列和
int n;
const int N = 100010;
int a[N];
int f[N];//以第i个元素结尾的最大子序列和
int res;

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];

    for (int i = 1; i <= n; i++) {
        f[i] = a[i]; // 最大上升子序列个数这里是1,此处是a[i]
        for (int j = 1; j < i; j++)
            // 最大上升子序列个数这里是加1,此处是+a[i]
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + a[i]);
        res = max(res, f[i]);
    }
    printf("%d ", res);
    return 0;
}

魔改 最长上升子序列和

AcWing 1010. 拦截导弹

数组说明 q[],ql:已经创建了ql套导弹系统,q[i]中记录的是第i套导弹系统的最大拦截高度

算法思路

  • 在已有的所有导弹拦截系统中找到大于当前高度,并且最小的那个,把当前导弹用这套拦截系统进行拦截
  • 如果找不到这样的拦截系统,说明当前所有拦截系统的最小拦截高度都小于当前导弹,都无法利用,必须新开一个拦截系统。
  • 只有前面所有拦截系统的最后一个拦截高度,都小于当前高度时,才会创建新的,所以,后面的拦截系统,它的尾巴值应该是最大的

举栗子 q[0]:5 q[1]:6 现在来了一个高度=4,根据上面的原则,应该放到q[0]里,修改q[0]=4 q[0]:4 q[1]:6 原来的数组是单调上升的,修改后也依然是单调上升的。

再比如: q[0]:4 q[1]:6 q[2]:7 现在来了一个a[i]=5,我们肯定要修改q[1]=5,变为:

q[0]:4 q[1]:5 q[2]:7 原来的数组是单调上升的,修改后也依然是单调上升的。

既然有这个规律,那么就可以使用二分快速查找大于等于a[i]的位置p了:

  // 第一问
    for (int i = 0; i < n; i++) {
        f[i] = 1;
        for (int j = 0; j < i; j++)
            if (a[i] <= a[j]) // 如果前面的比当前的大,说明是不升序列
                f[i] = max(f[i], f[j] + 1);
        res = max(res, f[i]);
    }
    // 第二问
    for (int i = 0; i < n; i++) {       
        int p = lower_bound(q, q + ql, a[i]) - q;
        if (p == ql) // 如果没有找到可以接在后面的导弹拦截系统,那么需要创建一套新的拦截系统
            q[ql++] = a[i];
        else
            q[p] = a[i]; // 否则就直接接到找到的第k套拦截系统的后面,那么第k套拦截系统的最后一个拦截高度=q[k]=h[i]
    }
    // 输出最长不升序列长度,即:最多能拦截的导弹数
    printf("%d\n%d\n", res, ql);

AcWing 187. 导弹防御系统

导弹拦截系统的拦截高度即可以 严格单调上升又可以严格单调下降此时用LIS模型是无法解决问题的考虑到n<50,可以用dfs+剪枝

#include <bits/stdc++.h>

using namespace std;
const int N = 55;
const int INF = 0x3f3f3f3f;
int n;
int a[N];
int up[N], down[N];
int res;

// 本题关键字:贪心+爆搜

/*
 功能:暴搜所有可能,配合剪枝,找出最少的拦截系统数量
 u:  第几个导弹
 ul: 上升拦截系统的数量配合up  数组使用
 dl: 下降拦截系统的数量配合down数组使用
 */
void dfs(int u, int ul, int dl) {
    if (ul + dl >= res) return;  // 伟大的剪枝不剪枝会TLE~,中途发现已经大于等于res的情况就返回
    if (u == n) {                // 走完全程,收集答案
        res = min(res, ul + dl); // 因为上面的剪枝把ul+dl>=res的全干掉了能到这里的都是<res的都可以用来更新答案
        return;
    }

    // 放入上升序列
    int k = 0;
    // 如果把当前导弹使用上升序列的拦截系统进行拦截,那个选择哪个系统呢?
    for (k = 0; k < ul; k++)
        if (up[k] <= a[u]) break; // up[0],up[1],up[2],... 这样套拦截系统,其数值来讲,是递减的,因为是因为不能再拦截高度更低的才创建了一套新的拦截系统
    // 找出放到哪个拦截系统上去结果为k

    int t = up[k]; // 尝试在第k套系统进行拦截第u个导弹,那么第u个导弹的高度就是第k套系统的新高度
    up[k] = a[u];  // 问题是我们也不一定非得让第u个导弹使用上升序列拦截系统也可能使用下降序列拦截系统所以需要记录当前值回溯后尝试下降序列拦截

    if (k < ul)                 // 如果当前这些套拦截系统中可以找到某一套进行拦截
        dfs(u + 1, ul, dl);     // 接到了某个队伍后面去了,修改队伍的最后最大值即可,队伍数量没有长大
    else                        // 如果当前这些套拦截系统中无法找到某一套进行拦截
        dfs(u + 1, ul + 1, dl); // 没有找到任何一个符合最后一个高度小于a[u]的队伍只能多开一队up数组长度长大1

    up[k] = t; // 回溯

    // ----------------------------------------------------------------------

    // 放入下降序列
    k = 0;
    for (k = 0; k < dl; k++)
        if (down[k] >= a[u]) break;

    t = down[k]; // 保留现场
    down[k] = a[u];
    if (k < dl)
        dfs(u + 1, ul, dl);
    else
        dfs(u + 1, ul, dl + 1);
    down[k] = t; // 回溯
}

int main() {
    while (cin >> n, n) { // 多套数据输入n=0时停止
        for (int i = 0; i < n; i++) cin >> a[i];
        res = INF;    // 防御系统的最少数量
        dfs(0, 0, 0); // 开始深搜更新res的值
        printf("%d\n", res);
    }
    return 0;
}

AcWing 272. 最长公共上升子序列

#include <bits/stdc++.h>

using namespace std;
const int N = 3010;
int a[N], b[N];
int f[N][N];
int res;

// O(n^2)
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];

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

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

    return 0;
}

最长上升子序列 (LIS) 详解+例题模板 (全)