1 条题解

  • 0
    @ 2025-8-24 21:20:50

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar VitrelosTia
    What you want, What you do ?

    搬运于2025-08-24 21:20:49,当前版本为作者最后更新于2023-04-09 22:07:57,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    前言

    题解区大多使用的都是时间复杂度 O(n2)O(n^2) 的算法,唯一使用二分查找的 O(nlogn)O(n \log n) 做法的被 hack 了,所以写一篇二分查找 O(nlogn)O(n \log n) 复杂度的题解。

    upd(2025.4.5):这篇题解是我远古时期写的,当时只是想介绍一种没什么用的解法,因为一些比较神秘的原因现在是第一篇,所以我打算新增一些内容。

    题意分析

    题目中要求的“合唱队形”满足的要求其实就是:t1,t2,t3,,tit_1, t_2, t_3, \dots, t_i 成上升序列,ti,ti+1,ti+2,,tnt_i, t_{i + 1}, t_{i + 2}, \dots, t_n 成下降序列。题目中要求算出最少出列人数,其实就是要求哪个 ii 能使得以 aia_i 为结尾的最长上升子序列和最长下降子序列之和最大。

    做法

    这道题想要用 O(nlogn)O(n \log n) 的复杂度做,首先要用 O(nlogn)O(n \log n) 求出最长上升子序列(下面简称 LIS)的长度。

    dp 的做法我放在后面了,有需要的朋友可以翻到后面看。

    O(nlogn)O(n \log n) 求 LIS 长度

    O(n2)O(n^2) 的做法中,求 LIS 长度的做法需要通过枚举两个数,假如是满足上升的,就将长度更新为原长度和新长度加一的最大值。通过这个朴素做法我们可以发现,以当前数结尾的 LIS 长度是否能达到 kk,取决于它是否能比一个长度为 k1k - 1 的序列的结尾数大。我们可以观察一组数据。

    该组数据中,我们记下以每个数结尾的 LIS 长度,由于上面所讲的,对于长度为 kk 的数据,我们只需要关注结尾数最小的,因为这代表它有可能产生的长度为 k+1k + 1 的 LIS 长度最多。于是我们可以去掉一些数据。

    还是上面的道理,我们把还留着的数据的上方的数放到一个数轴上,这样当我们要查询一个数的 LIS 长度时,只需要看这个数处于哪个区间中,它的 LIS 长度就是往前(不包括本身)最靠近它的数所对应的 LIS 长度加一。

    那么可以很快找到 66 的 LIS 长度为 3+1=43 + 1 = 4。这一个找区间的步骤使用复杂度 O(logn)O(\log n) 的二分查找。

    实现其实并不困难,按照上述的步骤做就行了。

    #include <bits/stdc++.h>
    using namespace std;
    
    const int M = 1e5 + 5, INF = 1e9;
    int n, a[M];
    int f[M], g[M], len;
    // f[i]   a[i] 为结尾的 LIS 长度
    // g[i]   上升子序列长度为 i 时结尾最小值
    // len    LIS 长度
    
    int main() {
    	int n;
    	scanf ("%d", &n);
    	for (int i = 1; i <= n; i++) scanf ("%d", &a[i]);
    	for (int i = 1; i <= n; i++) {
    		int pos = lower_bound(g + 1, g + len + 1, a[i]) - g; //二分查找区间
    		f[i] = pos;
    		g[pos] = a[i];//查找到之后还要更新最小值
    		len = max(len, pos);
    	}
    	cout << *max_element(f + 1, f + n + 1); //输出最长的 LIS 长度
    	return 0;
    }
    

    实际上,我们并不需要 f[] 数组,可以直接输出 len。但在本题中需要记录以每个数结尾的 LIS 长度,所以这样写了。

    这种做法的时间复杂度是 O(nlogn)O(n \log n)。依照相同的思路,最长下降子序列长度的求法只需要倒着枚举 i 即可。

    本题解法

    如我们分析的题意一样,本题只需要记录以 aia_i 为最后一个数的最长上升子序列长度和以 aia_i 为第一个数的最长下降子序列长度即可,我们用 f1[]f2[] 来记录这两组数,第 aia_i 个数的最长合唱队形就是 f1[i] + f2[i] - 1(中间有重合所以减去 11)。用总人数减去最大值就是答案了。

    #include <bits/stdc++.h>
    using namespace std;
    
    const int M = 1e5 + 5, INF = 1e9;
    int a[M], f1[M], f2[M], g[M], len, ans = -INF;
    
    int main() {
        int n;
        scanf ("%d", &n);
        for (int i = 1; i <= n; i++) scanf ("%d", &a[i]);
        len = 0;
        for (int i = 1; i <= n; i++) {
            int pos = lower_bound(g + 1, g + len + 1, a[i]) - g;
            f1[i] = pos;
            g[pos] = a[i]; 
            len = max(len, pos);
        }
        len = 0;
        memset(g, 0, sizeof g);
        for (int i = n; i >= 1; i--) {
            int pos = lower_bound(g + 1, g + len + 1, a[i]) - g;
            f2[i] = pos;
            g[pos] = a[i];
            len = max(len, pos);
        }
        for (int i = 1; i <= n; i++) ans = max(ans, f1[i] + f2[i] - 1);
        cout << n - ans;
        return 0;
    }
    

    dp 的想法

    还是介绍一下比较有用的 dp 吧。

    考虑设 fif_i 表示考虑完前 ii 个数,强制ii 结尾的最长上升子序列。枚举上一个选的位置 jj,需要满足 aj<aia_j < a_i,我们在 jj 处已经求得以 jj 结尾的最长的长度是 fjf_j,于是 fifj+1f_i \gets f_j+1 即可。可以写成式子:

    $$f_i=\max_{j=1}^{i-1} f_j+1 \ \ \texttt{s.t.} a_j<a_i$$。 ### 优化至 $O(n \log n)$ 考虑维护一个值域数据结构(你可以认为是一个树状数组)并做扫描线(你可以认为是从 $1$ 到 $n$ 遍历),扫描到 $i$ 时,在值域 $a_i$ 处用 $f_i$ 更新最大这个值域的最大值,这样,我们可以通过查询值域 $[0,a_i-1]$ 的最大值来获得满足 $a_j < a_i$ 的最大 $f_j$。可以用树状数组或者线段树实现。时间复杂度 $O(n\log n)$。$$
    • 1

    信息

    ID
    93
    时间
    1000ms
    内存
    125MiB
    难度
    2
    标签
    递交数
    1
    已通过
    1
    上传者