草庐IT

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

孤星·璀璨 2023-03-28 原文

本题为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;
}

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

有关「最长上升子序列」合唱队形的更多相关文章

  1. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  2. ruby - 在 Ruby 中比较序列 - 2

    假设我必须(小型到中型)阵列:tokens=["aaa","ccc","xxx","bbb","ccc","yyy","zzz"]template=["aaa","bbb","ccc"]如何确定tokens是否以相同的顺序包含template的所有条目?(请注意,在上面的示例中,应忽略第一个“ccc”,从而由于最后一个“ccc”而导致匹配。) 最佳答案 这适用于您的示例数据。tokens=["aaa","ccc","xxx","bbb","ccc","yyy","zzz"]template=["aaa","bbb","ccc"]po

  3. ruby-on-rails - carrierwave:在序列化动态属性上安装 uploader - 2

    首先,我使用的是rails3.1.3和来自master的carrierwavegithub仓库的分支。我使用after_init钩子(Hook)来确定基于属性的字段页面模型实例并为这些字段定义属性访问器将值存储在序列化哈希中(希望它清楚我是什么谈论)。这是我正在做的事情的精简版:classPage省略mount_uploader命令让我可以访问我想要的属性。但是当我安装uploader时出现错误消息说“nil类的未定义新方法”我在源代码中读到有方法read_uploader和扩展模块中的write_uploader。我如何必须覆盖这些来制作mount_uploader命令使用我的“虚拟

  4. ruby-on-rails - 在 Elastic Beanstalk 上升级 Ruby - 2

    如何在ELB上设置和更新ruby​​版本?我已经在我们的qa和暂存环境中使用ruby2.2.2大约8个月了。我刚刚在星期一设置我们的生产环境,它不会部署,因为它说ruby​​设置为2.2.3而我的gemfile说2.2.2。我更新并重新部署,一切似乎都很好。我回到了qa/staging环境,但无法将其更新到ruby​​2.2.3。一直说ruby版本是2.2.2,Gemfile是2.2.3我升级了(通过elbui):运行Ruby2.2(PassengerStandalone)的64位AmazonLinux2015.03v1.3.1到运行Ruby2.2(PassengerStandalon

  5. 机器学习——时间序列ARIMA模型(四):自相关函数ACF和偏自相关函数PACF用于判断ARIMA模型中p、q参数取值 - 2

    文章目录1、自相关函数ACF2、偏自相关函数PACF3、ARIMA(p,d,q)的阶数判断4、代码实现1、引入所需依赖2、数据读取与处理3、一阶差分与绘图4、ACF5、PACF1、自相关函数ACF自相关函数反映了同一序列在不同时序的取值之间的相关性。公式:ACF(k)=ρk=Cov(yt,yt−k)Var(yt)ACF(k)=\rho_{k}=\frac{Cov(y_{t},y_{t-k})}{Var(y_{t})}ACF(k)=ρk​=Var(yt​)Cov(yt​,yt−k​)​其中分子用于求协方差矩阵,分母用于计算样本方差。求出的ACF值为[-1,1]。但对于一个平稳的AR模型,求出其滞

  6. ruby-on-rails - Rails 编辑序列化的 JSON 数据 - 2

    我有一个存储JSON数据的列。当它处于编辑状态时,我不知道如何显示它。serialize:value,JSON=f.fields_for:valuedo|ff|.form-group=ff.label:short=ff.text_field:short,class:'form-control'.form-group=ff.label:long=ff.text_field:long,class:'form-control' 最佳答案 代替=f.fields_for:valuedo|ff|请使用以下代码:=f.fields_for:va

  7. ruby-on-rails - 序列化时无法将空数组保存到数据库 - 2

    在RubyonRails中,如果数组为空,则具有序列化数组字段的模型将不会在.save()上更新,而它之前有数据。我正在使用:ruby2.2.1rails4.2.1sqlite31.3.10我创建了一个字段设置为文本的新模型:railsgmodel用户名:stringexample:text在我添加的User.rb文件中:serialize:example,Array我实例化了User类的一个新实例:test=User.new然后我保存用户以确保它正确保存:test.save()(0.1ms)begintransactionSQL(0.4ms)INSERTINTO"users"("cr

  8. ruby - 加载使用 YAML 序列化的对象时调用初始化 - 2

    是否可以在使用YAML.load_file时强制Ruby调用初始化方法?我想调用该方法以便为我不序列化的实例变量提供值。我知道我可以将代码分解成一个单独的方法并在调用YAML.load_file之后调用该方法,但我想知道是否有更优雅的方法来处理这个问题。 最佳答案 我认为你做不到。由于您要添加的代码确实特定于要反序列化的类,因此您应该考虑在类中添加该功能。例如,让Foo成为您要反序列化的类,您可以添加一个类方法,例如:classFoodefself.from_yaml(yaml)foo=YAML::load(yaml)#editth

  9. ruby-on-rails - FactoryGirl工厂特征内的序列不使用主序列计数器 - 2

    我有以下工厂:FactoryGirl.definedofactory:foodosequence(:name){|n|"Foo#{n}"}trait:ydosequence(:name){|n|"Fooy#{n}"}endendend如果我跑create:foocreate:foocreate:foo,:y我得到Foo1,Foo2,Fooy1。但我想要Foo1,Foo2,Fooy3。我怎样才能做到这一点? 最佳答案 经过smile2day'sanswer的一些提示后和thisanswer,我得出以下解决方案:FactoryGirl.

  10. ruby - 在 ruby​​ 中合并两个排序列表的内置方法 - 2

    我有两个Foo对象列表。每个Foo对象都有一个时间戳,Foo.timestamp。两个列表最初都按时间戳降序排列。我想以最终列表也按时间戳降序排序的方式合并Foo对象的两个列表。实现这个并不难,但我想知道是否有任何内置的Ruby方法可以做到这一点,因为我认为内置方法会产生最佳性能。谢谢。 最佳答案 这会起作用,但不会提供很好的性能,因为它不会利用事先已经排序的列表:list=(list1+list2).sort_by(&:timestamp)我不知道有任何内置函数可以满足您的需求。 关于

随机推荐