合唱队形

GitHub跳转原题关系图返回列表

以每个位置为峰值,正向 LIS 得左段、反向 LIS 得右段,rise[i]+fall[i]-1 的最大值即最多保留人数,N 减之即为答案。

OJ: roj

题目 ID: 1264

难度:普及-

标签:DPLIS动态规划

创建: 2026-09-30 02:25

更新: 2026-10-05 07:28

题目描述

NN 位同学排成一排,老师想让其中的 (N−K)(N-K) 位出列,使剩下的 KK 位同学身高呈“先严格上升、后严格下降”的合唱队形。给定 NN 和各位同学的身高,求最少需要出列的同学数。2≤N≤1002 \le N \le 100,130≤Ti≤230130 \le T_i \le 230。

输入第一行为 NN,第二行为 NN 个身高;输出一行,为最少出列人数。

样例输入:

text
8
186 186 150 200 160 130 197 220

样例输出:

text
4

思路

以每个位置 ii 为峰值,左段是以 ii 结尾的最长严格上升子序列 rise[i],右段等价于把序列反转后求一次同样的严格上升子序列 fall[i]。峰值被算了两次,所以最多保留人数为 max⁡i(rise[i]+fall[i]−1)\max_i\big(\text{rise}[i]+\text{fall}[i]-1\big),答案为 NN 减去该值。

参考代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 02:25
# update_at: 2026-09-30 02:25

import sys


def lis_heights(nums: list[int]) -> list[int]:
    """以每个位置结尾、严格上升子序列的最大长度(LIS 经典 O(n^2) DP)。"""
    dp: list[int] = []  # dp[i] = 以 nums[i] 结尾的 LIS 长度
    for i, height in enumerate(nums):
        dp.append(1 + max((dp[j] for j in range(i) if nums[j] < height), default=0))
    return dp


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)
    heights = [next(data) for _ in range(n)]  # 每位同学的身高

    rise = lis_heights(heights)                # rise[i]:i 左侧(含 i)的最长严格上升
    fall = lis_heights(heights[::-1])[::-1]    # fall[i]:i 右侧(含 i)的最长严格下降

    # 峰值算两次,减 1;n<=2 时队列必然不严格,max 兜底为 0,答案输出 n
    keep = max((r + f - 1 for r, f in zip(rise, fall)), default=0)
    print(n - max(keep, 0))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-05 07:27
 * update_at: 2026-10-05 07:27
 */

#include <iostream>
#include <algorithm>
using namespace std;

typedef long long ll;

const int MAXN = 105;

ll h[MAXN];      // 每位同学的身高
ll rise[MAXN];   // rise[i]:以 i 结尾的最长严格上升子序列长度
ll fall[MAXN];   // fall[i]:从 i 开始的最长严格下降子序列长度

// 对数组 a[1..n] 求以每个位置结尾的最长严格上升子序列长度
void lis_strict(ll a[], ll dp[], ll n) {
    for (ll i = 1; i <= n; i++) {
        dp[i] = 1; // 至少包含自己
        for (ll j = 1; j < i; j++) {
            if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
            }
        }
    }
}

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

    ll n;
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> h[i];
    }

    lis_strict(h, rise, n); // 正向求严格上升

    // 反转序列求“严格上升”,对应原序列的“严格下降”
    ll rev_h[MAXN];
    ll rev_dp[MAXN];
    for (ll i = 1; i <= n; i++) {
        rev_h[i] = h[n - i + 1];
    }
    lis_strict(rev_h, rev_dp, n);
    for (ll i = 1; i <= n; i++) {
        fall[i] = rev_dp[n - i + 1];
    }

    ll keep = 0; // 最多能保留的同学数
    for (ll i = 1; i <= n; i++) {
        ll total = rise[i] + fall[i] - 1; // 峰值被算了两次,减 1
        if (total > keep) {
            keep = total;
        }
    }

    cout << n - keep << "\n";
    return 0;
}