【算法】合唱队形问题&最大上升子序列 详细代码+分析(C++)

文章介绍了如何解决找到最少需要移除的同学,使剩余同学能形成合唱队形的问题,即寻找最大上升子序列。代码中通过两个方向分别计算最大上升子序列,结合求得最终答案。解释了代码中循环的作用,确保所有可能的上升子序列都被考虑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在线判题通道:牛客网-HJ24 合唱队

题目描述:

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

通俗来说,能找到一个同学,他的两边的同学身高都依次严格降低的队形就是合唱队形。

例子:

123 124 125 123 121 是一个合唱队形

123 123 124 122不是合唱队形,因为前两名同学身高相等,不符合要求

123 122 121 122不是合唱队形,因为找不到一个同学,他的两侧同学身高递减。

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

注意:不允许改变队列元素的先后顺序 且 不要求最高同学左右人数必须相等

输入描述:

用例两行数据,第一行是同学的总数 N ,第二行是 N 位同学的身高,以空格隔开

输出描述:

最少需要几位同学出列

示例1

输入:

8

186 186 150 200 160 130 197 200

输出:

4

说明:

由于不允许改变队列元素的先后顺序,所以最终剩下的队列应该为186 200 160 130或150 200 160 130

代码(详细注释代码在下面):

#include<bits/stdc++.h>
using namespace std;
int n,a[3001],f1[3001],f2[3001],MAX;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)    {
        cin>>a[i];
        f1[i] = 1;
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])
            {
                f1[i]=max(f1[i],f1[j] + 1);                
            }
        }

    }
    for(int i=n;i>=1;i--)
    {
        f2[i] = 1;
        for(int j=n;j>i;j--)
        {
            if(a[j]<a[i])
            {
                f2[i] = max(f2[i], f2[j] + 1);
            }
        }
        MAX=max(MAX,f1[i]+f2[i]-1);
    }
    cout<<n-MAX<<endl;
    return 0;
 } 

题解+详细注释版代码

合唱队问题归结下来是求两个最大上升子序列的长度问题,从队列正方向得到所有的最大上升子序列,再从反方向得到所有的最大上升子序列(即为最大下降子序列),找出两个组合起来总长度最大的情况即为最终答案,所以问题简化为求最大上升子序列的问题。

首先这个是课本上的内容,已经介绍的非常详细,如果看不太懂的话建议配合课本给出的例题,推算一下实际每步产生的队列是哪几个具体的同学,就会非常清晰了

如何理解代码&我遇到的问题&解决思路&代码理解

下面我就带着大家,从我当时理解算法&写代码的时候遇到问题问题出发,带大家理解一下代码

我的在coding过程中的最大问题不是对最大上升子序列的理解没到位,所以上面的介绍就简单过,主要是下面谈一下我遇到的代码上的最大的问题:

问题:在寻找最大上升子序列时 书上的动归方程给出的是

但是实际在代码中,没有能对一个集合内多个元素同时求出一个最大值的操作 所以需要使用一个循环来遍历实现,我就是在这个循环的地方卡了很久的理解:

    for(int i=1;i<=n;i++)    {
        cin>>a[i];
        f1[i] = 1;//问题①:方程中只给出了f1[1] = 1的递归出口
        //但是在代码中是将所有的f1[i]的初始值都给成了1
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])
            {
                f1[i]=max(f1[i],f1[j] + 1);
                //问题②:一直不能理解这个地方,只求了两个数对比的最大值,如果出现1 2 5 3这样的串
                //那么是不是再次到3的时候 因为5 > 3 所以f1[4]的值只能为1了             
            }
        }

然后我想着搞不懂就自己推一遍过程

于是得到了答案:

问题①:给数组内所有元素赋值为1是为了防止出现如2 5 1这样的排列的时候到了第三个同学高度为1 则此时最大上升子序列的值应该为1,如果不整体赋初值的话,到第三个同学这里,上升序列的长度就是0了

但是这好像仍然不能解决问题②,于是我研究了下这个循环

这里使用了循环for(int j=1;j<i;j++) 遍历了所有的a[j]与a[i]比较 就是为了保证不会出现1 2 5 3的情况时

a[3] = 5 > a[4] = 3 导致f[4] = 1

而使用循环会保证和前面所有的上升子串都进行了一次比较

j=1:a[1] < a[4] = 3 --> f[4] = f1[1] + 1 = 2

表示序列f1[1] + 第4个同学可以组成新序列1 3且长度为2

j=2:a[2] < a[4] = 3 --> f[4] = f1[2] + 1 = 3

表示序列f2[1] + 第4个同学可以组成新序列1 2 3且长度为3

j=1:a[3] > a[4] = 3 --> f[4] = f1[2] = 3

5 > 3 则第三个序列f[3]无法和第4个同学组成新序列 序列长度还是之前的3

得出序列f[4]最大值为3

所以for(int j=1;j<i;j++) 这个j循环就是负责了这个集合中找最大值的功能,对应了方程

f1[j]代表到第j个同学的最大上升子串 则为了求集合最大值 使用了一次循环遍历

如果a[i] > a[j]说明第i个同学要比第j个同学 和第j个同学已经建立起的上升子序列中所有同学都高,则上升子序列的最大值,就可以更新为已经建立的上升子序列f1[j]加上同学a[i] 表示为f1[j] + 1

详细注释版代码


#include<bits/stdc++.h>
using namespace std;
int n,a[3001],f1[3001],f2[3001],MAX;//f1[i]表示上升子序列的个数 f2[i]表示反方向开始的上升子序列的个数


int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)//下标从1开始
    {
        cin>>a[i];
        f1[i] = 1;//这里很关键 递归边界是f[1] = 1 以及每个上升子序列的最小值为1
        //但是给数组内所有元素赋值为1是为了防止出现如2 5 1这样的排列的时候
        //到了第三个同学高度为1 则此时最大上升子序列的值应该为1
        for(int j=1;j<i;j++)//利用循环求集合中的最大值max{f[j] + 1}需要循环 因为程序只能求两者最大值
        {
            if(a[j]<a[i])//如果a[i]比a[j]高 则说明出现了更高的同学 上升子序列可以增加
            //但是这里注意 j是从1开始遍历到i-1 说明是从a[j]开始 一个一个与a[i]比较了
            //f[j]代表到第j个同学的最大上升子串 则为了求集合最大值 使用了一次循环遍历
            //如果a[i] > a[j]说明第i个同学要比第j个同学 和第j个同学已经建立起的上升子序列中所有同学都高
            {
                f1[i]=max(f1[i],f1[j] + 1);             
                //则上升子序列的最大值,就可以更新为已经建立的上升子序列f[j]加上同学a[i] 表示为f[j] + 1
                //整个for j 循环满足动归方程 f[i] = max{f[j] + 1} (1 < j < i <= n)
            }
            //但是这里使用了循环 遍历了所有的a[j]与a[i]比较 就是为了保证不会出现1 2 5 3的情况时
            //a[3] = 5 > a[4] = 3 导致f[4] = 1
            //而使用循环会保证和前面所有的上升子串都进行了一次比较
            //j=1:a[1] < a[4] = 3 --> f[4] = f1[1] + 1 = 2
            //表示序列f1[1] + 第4个同学可以组成新序列1 3且长度为2
            //j=2:a[2] < a[4] = 3 --> f[4] = f1[2] + 1 = 3
            //表示序列f2[1] + 第4个同学可以组成新序列1 2 3且长度为3
            //j=1:a[3] > a[4] = 3 --> f[4] = f1[2] = 3
            //5 > 3 则第三个序列f[3]无法和第4个同学组成新序列 序列长度还是之前的3
            //得出序列f[4]最大值为3
        }
    }

    for(int i=n;i>=1;i--)
    {
        f2[i] = 1;
        for(int j=n;j>i;j--)
        {
            if(a[j]<a[i])
            {
                f2[i] = max(f2[i], f2[j] + 1);
            }
        }
        MAX=max(MAX,f1[i]+f2[i]-1);//遍历所有的f1和f2算出最大值
        //因为上升和下降的个数都包括了最高点的同学,所以减去一个
    }
    cout<<n-MAX<<endl;
    return 0;
 } 

&lt;think&gt;我们解决合唱队问题,该问题可以转化为:寻找一个位置i,使得以i为最高点,左边是严格递增的,右边是严格递减的。我们需要让这样的队形人数最多,那么出列的人数就最少。 具体做法: 1. 计算每个位置i的最长递增子序列(从左到右,以i结尾) 2. 计算每个位置i的最长递减子序列(从右到左,以i结尾) 3. 对于每个位置i,将f[i](递增子序列长度)和g[i](递减子序列长度)相加再减1(因为i被重复计算了一次),得到以i为最高点时合唱队的总人数。 4. 遍历所有i,得到最大的总人数max_num,那么最少需要出列的人数为:n - max_num。 注意:题目中的队形要求是中间高,两边低,并且严格递增和严格递减。 根据引用[1][2][3][4]中的代码,我们可以写出如下C++代码代码步骤: 1. 输入n和n个同学的身高,存储在数组a中(下标从1开始或0开始,这里我们使用1开始)。 2. 初始化两个数组f和g,f[i]表示以a[i]结尾的最长递增子序列长度,g[i]表示以a[i]结尾的最长递减子序列长度(注意:从右向左看是递增,从左向右看就是递减)。 3. 正向遍历计算f[i]:对于每个i,从j=1到i-1,如果a[j]&lt;a[i],则f[i]=max(f[i], f[j]+1)。 4. 反向遍历计算g[i]:对于每个i(从n到1),从j=n到i+1,如果a[j]&lt;a[i],则g[i]=max(g[i], g[j]+1)。注意这里从右向左看,递减子序列相当于从右向左的递增子序列。 5. 遍历每个位置i,计算f[i]+g[i]-1,并求最大值max_num。 6. 输出n - max_num。 注意:在计算递减子序列时,我们也可以换一种思路:将数组逆序,然后求递增子序列,再逆序回来。但这里我们直接反向求递增子序列(即原数组的递减子序列)。 我们使用0-indexed数组,即数组下标从0开始。 参考引用[1]和[4]的代码,我们写一个C++程序。 注意:引用[4]的代码中,Dec[i]表示从右向左以i为结尾的最长递增子序列(即原数组中从i开始向右看是递减的),所以g[i](即Dec[i])就是我们要的递减子序列长度。 下面给出C++代码&lt;/think&gt;### 合唱队问题解析(动态规划解法) 合唱队问题本质上是求**先递增后递减的最长子序列**,可以通过动态规划求解。核心思路是计算每个位置的最长递增子序列和最长递减子序列,然后找到最优的&quot;峰顶&quot;位置。 #### 算法步骤 1. **计算最长递增子序列**(从左向右) $f[i]$ 表示以 $a[i]$ 结尾的最长递增子序列长度 $f[i] = \max(f[j]) + 1$,其中 $j &lt; i$ 且 $a[j] &lt; a[i]$ 2. **计算最长递减子序列**(从右向左) $g[i]$ 表示以 $a[i]$ 结尾的最长递减子序列长度 $g[i] = \max(g[j]) + 1$,其中 $j &gt; i$ 且 $a[j] &lt; a[i]$ 3. **寻找最优峰顶** 合唱队最大人数:$\max(f[i] + g[i] - 1)$ 最少出列人数:$n - \max(f[i] + g[i] - 1)$ #### C++ 完整代码实现 ```cpp #include &lt;iostream&gt; #include &lt;algorithm&gt; using namespace std; const int N = 110; int n; int a[N], f[N], g[N]; int main() { cin &gt;&gt; n; for (int i = 1; i &lt;= n; i++) cin &gt;&gt; a[i]; // 计算最长递增子序列 (从左向右) for (int i = 1; i &lt;= n; i++) { f[i] = 1; for (int j = 1; j &lt; i; j++) { if (a[j] &lt; a[i]) { f[i] = max(f[i], f[j] + 1); } } } // 计算最长递减子序列 (从右向左) for (int i = n; i &gt;= 1; i--) { g[i] = 1; for (int j = n; j &gt; i; j--) { if (a[j] &lt; a[i]) { g[i] = max(g[i], g[j] + 1); } } } // 寻找最优峰顶位置 int max_num = 0; for (int i = 1; i &lt;= n; i++) { max_num = max(max_num, f[i] + g[i] - 1); } cout &lt;&lt; n - max_num &lt;&lt; endl; return 0; } ``` #### 代码说明 1. **输入格式** - 第一行:同学总数 $n$ - 第二行:$n$ 个整数表示身高序列 2. **时间复杂度** $O(n^2)$,适用于 $n \leq 1000$ 的数据规模 3. **示例测试** 输入: ``` 8 186 186 150 200 160 130 197 220 ``` 输出: ``` 4 ``` #### 算法优化点 1. **二分优化**:将 LIS 计算优化到 $O(n \log n)$ 2. **空间优化**:使用滚动数组减少空间复杂度 3. **边界处理**:增加空序列和单元素序列的特殊处理 &gt; 该解法基于动态规划的最长子序列模型,通过正反两次扫描确定最优峰顶位置[^1][^2][^3][^4]。 --- ### 相关问题 1. 如何将算法优化到 $O(n \log n)$ 时间复杂度? 2. 如果允许相同身高相邻,算法需要如何调整? 3. 如何输出具体的合唱队排列方案? 4. 该算法能否推广到三维身高排列问题? 5. 动态规划与贪心算法子序列问题中的本质区别是什么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

P1nkB1ood

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值