「最长上升子序列」合唱队形

本题为3月15日23上半学期集训每日一题中A题的题解

题面

题目描述

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

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

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

输入

输入文件chorus.in的第一行是一个整数N( \(2 \leq N \leq 100\) ),表示同学的总数。第二行有N个整数,用空格分隔,第i个整数 \(T_i( 130 \leq T_i \leq 230 )\) 是第i位同学的身高(厘米)。

输出

输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

样例输入

8
186 186 150 200 160 130 197 220

样例输出

4

提示

对于50%的数据,保证有 $n\leq 20 $ ;对于全部的数据,保证有 \(n\leq 100\)

思路分析

本题需要用到最长上升子序列的纯dp解法(二分优化版本当然也可使用,考虑有些同学可能刚学最长上升子序列,所以我把二分优化版本放在拓展解法中),如果你不会最长上升子序列,请自行搜索学习,可以阅读这篇博客.并在学会后独立AC洛谷上的模板题(ACWing上也有一样的模板题,如果不喜欢洛谷可以去ACWing上做这道题,你要是都不喜欢还可以去LeetCode上做第300题)以保证你已学会.

此题的题意就是要求我们从原本的一排同学中移去几个人,使得最后形成的队伍中,左边一半是单调上升的,右边一半是单调下降的.而相对于原本的那排同学来说,这个移去几个人所形成的队伍,其实就是它的一个子序列,其左半段是一个上升的子序列,而右半段是一个下降的子序列.题目要求移去的人数最少,也就是剩下的人数最多,也就是这个子序列的长度要最长.所以这道题其实就是要求我们去求原数组的一个左上升右下降的最长子序列的长度.

我们并不知道最终形成的子序列的极值点在哪,所以我们可以枚举每一个点,将它作为分段点,其对其左半段求它的最长上升子序列长度,对其的右半段求它的最长下降子序列的长度(这个点本身在左边还是在右边均可,我个人写的时候将它放在了右半段),两者相加就是以这个点为原序列的分段点时(注意不是以这个点为极值点,这个点可以不在最终的序列中)最终形成的子序列的最长长度.最后取所有点分段的结果中最大的那个即可.

直接使用上述思路可以得到一个 \(O(N^3)\) 的一个解法(最长上升子序列长度使用纯dp求解,如果用二分优化就是 \(O(N^2logN)\) ),我们还可以继续优化.我们可以注意到,在一次对整个数组求解最长上升子序列的过程中,我们其实是可以直接得到每一个 \([0, i](0 \leq i \leq n - 1)\) 区间的最长上升子序列的长度的.

// 这是一段最长上升子序列的模板,放在这里主要是为了让你知道我下面说的res和dp数组是哪个东西
int LIS(int *a, const int &n) {
    int res = 0xc3c3c3c3;

    int *dp = new int[n];

    for (int i = 0; i < n; i++) {
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (a[j] < a[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
            res = max(res, dp[i]);
        }
    }
    delete[] dp;
    return res;
}

在上面这段求解最长上升子序列的代码中,变量res存放的其实就是 \([0,i]\) 区间的最长上升子序列的长度,因为这个res的值会始终保持为 \([0,i]\) 区间上dp数组的最大值,也就是这个区间上最长上升子序列的长度.所以res自身会在循环的过程中取遍每一个 \([0, i](0 \leq i \leq n - 1)\) 区间的最长上升子序列的长度.我们可以把它从一个变量改成一个数组,即用 \(res[i]\) 表示 \([0, i]\) 这一段的最长上升子序列的长度,根据原本代码中的 \(res = max(res, dp[i])\) ,可以得到它的状态转移方程:

\(res[i] = \begin{cases} dp[i], i = 0\\ max(res[i - 1], dp[i]), i > 0 \end{cases}\)

有了这个数组之后,我们就可以通过访问这个数组中的第i项,以常数时间得到以i为分段点时其左边的最长上升子序列的长度了.

对于最长下降子序列,也可采用同样的处理方式.不过,其实最长下降子序列没有必要再单独写一遍,因为如果我们把原数组倒过来,那么对它求出的最长上升子序列,就是原数组的最长下降子序列了,C++的STL中自带的reverse函数可以实现倒转数组的功能.这里注意由于原数组倒转了,所以最终求出来的存放结果的数组在算完后要再倒转回来.

参考代码

时间复杂度: \(O(N^2)\)

空间复杂度: \(O(N)\)

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

/*
    最长上升子序列模板
*/
int *LIS(int *a, const int &n) {
    int *res = new int[n]; // 每一个位置的最长上升子序列
    memset(res, 0xc3, sizeof(int) * n);

    int *dp = new int[n];
    for (int i = 0; i < n; i++) {
        dp[i] = 1;
        for (int j = 0; j < i; j++) {
            if (a[j] < a[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }

        // 原本模板里的更新变量res改成递推res[i]
        if (i != 0) {
            res[i] = max(res[i - 1], dp[i]); // 如果当前更长就用当前的,或者就用前面的
        } else {
            res[i] = dp[i];
        }
    }

    delete[] dp;
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int n;
    cin >> n;

    int *t = new int[n];
    for (int i = 0; i < n; i++) {
        cin >> t[i];
    }

    // 先跑一遍最长上升子序列
    int *dpI = LIS(t, n);

    // 把数组倒一下跑最长上升子序列,就是最长下降子序列
    reverse(t, t + n);
    int *dpD = LIS(t, n);
    reverse(dpD, dpD + n); // 记得结果要再倒回来

    // 计算题目所求
    int res = 0xc3c3c3c3;
    for (int i = 0; i < n; i++) {
        int a = i > 0 ? dpI[i - 1] : 0; // 当前左边的最长上升子序列长度
        int b = dpD[i]; // 当前右边的最长下降子序列的长度
        res = max(res, a + b);
    }
    cout << n - res << "\n"; // 别忘了题目求的是出去几个人

    delete[] t;
    delete[] dpI;
    delete[] dpD;
    return 0;
}

拓展解法

此外,本题中的求最长上升子序列长度的部分也可以改用使用二分优化的形式,根据这种算法的原理可知,此时前面res数组中存放的值便是维护的那个上升数组当前的实际长度,我们把这个长度放入res数组中即可.

时间复杂度: \(O(NlogN)\)

空间复杂度: \(O(N)\)

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

// 这段改成了二分优化的形式
int *LIS(int *a, const int &n) {
    int *res = new int[n];

    int *dp = new int[n];
    memset(dp, 0x3f, sizeof(int) * n);
    int idx = 1;
    dp[0] = a[0];
    for (int i = 0; i < n; i++) {
        if (a[i] > dp[idx - 1]) {
            dp[idx++] = a[i];
        } else {
            *lower_bound(dp, dp + idx, a[i]) = a[i];
        }
        res[i] = idx;
    }

    delete[] dp;
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int n;
    cin >> n;

    int *t = new int[n];
    for (int i = 0; i < n; i++) {
        cin >> t[i];
    }

    int *dpI = LIS(t, n);
    reverse(t, t + n);
    int *dpD = LIS(t, n);
    reverse(dpD, dpD + n);

    int res = 0xc3c3c3c3;
    for (int i = 0; i < n; i++) {
        int a = i > 0 ? dpI[i - 1] : 0;
        int b = dpD[i];
        res = max(res, a + b);
    }
    cout << n - res << "\n";

    delete[] t;
    delete[] dpI;
    delete[] dpD;
    return 0;
}

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德

热门相关:最强狂兵   薄先生,情不由己   刺客之王   豪门闪婚:帝少的神秘冷妻   大神你人设崩了