题解:学而思编程 改造过山车

【题目来源】

学而思编程:改造过山车

【题目描述】

游乐园的老板要改造他的过山车,小明接到任务,要对过山车的高度进行改造。过山车分为 \(n\) 小段,第 \(i\) 段由一个高度为 \(h_i\) 的支柱支撑。改造的方法很简单,拆除原来的支柱,重新安装一种高度可伸缩的支柱。

游乐园老板想要将过山车高改成先上升再下降的形式,也就是修改后的高度要满足存在一个 \(h_i\)\(2≤i≤n−1\)),使得 \(h_1<h_2<⋯<h_i>h_{i+1}>⋯>h_n\)。修改后的高度可以是小数。

问小明最少需要重新安装多少支柱?

【输入】

\(1\) 行,\(1\) 个正整数 \(n\)

\(2\) 行,\(n\) 个正整数 \(h_1,h_2,⋯ ,h_n\)

【输出】

\(1\) 个整数,输出最少需要重新安装的支柱数量。

【输入样例】

5
1 3 2 2 1

【输出样例】

1

【核心思想】

  1. 问题分析:给定 \(n\) 段过山车的高度 \(h_i\),需要改造成先严格上升再严格下降的形式(山峰形状),求最少需要重新安装多少支柱。等价于求最长的先增后减子序列(山峰型子序列),最少需要移除的支柱数 = \(n\) - 最长保留序列长度。这是一个双向LIS问题。

  2. 算法选择

    • 正向LISf1[i] 表示从左到右,以第 \(i\) 个支柱结尾的最长递增子序列长度
    • 反向LISf2[i] 表示从右到左,以第 \(i\) 个支柱结尾的最长递增子序列(即原序列中以 \(i\) 为起点向右的最长递减子序列)
    • 前缀/后缀最大值sf1[i] 表示前 \(i\) 个位置的最长递增子序列长度,sf2[i] 表示从第 \(i\) 个位置到末尾的最长递减子序列长度
  3. 关键步骤

    • 计算正向LIS:遍历 \(i\)\(1\)\(n\),对于每个 \(i\),遍历 \(j\)\(1\)\(i-1\),若 \(h[i] > h[j]\),则 f1[i] = max(f1[i], f1[j] + 1)
    • 计算反向LIS:遍历 \(i\)\(n\)\(1\),对于每个 \(i\),遍历 \(j\)\(i+1\)\(n\),若 \(h[i] > h[j]\),则 f2[i] = max(f2[i], f2[j] + 1)
    • 计算前缀/后缀最大值sf1[i] = max(sf1[i-1], f1[i])sf2[i] = max(sf2[i+1], f2[i])
    • 枚举峰值位置:对于每个位置 \(i\)
      • \(i\) 为峰值:f1[i] + f2[i] - 1(峰值被计算两次,减1去重)
      • 峰值在 \(i\) 左右两侧分离:sf1[i-1] + sf2[i+1]
    • 输出答案n - ans,其中 ans 是最长保留序列长度
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),两次LIS计算,每次 \(O(n^2)\)
    • 空间复杂度:\(O(n)\),四个数组 f1f2sf1sf2
  5. 双向LIS的核心思想

    • 问题转化:将"最少改造"转化为"最多保留",即求最长先增后减子序列
    • 两次LIS:分别计算从左到右的递增和从右到左的递增(即原序列的递减)
    • 峰值处理:枚举每个位置作为峰值,计算以该位置为转折点的最长山峰序列
    • 分离情况:考虑峰值在左右两侧分离的情况(两段独立序列)
    • 适用于山峰型、山谷型等双向单调性约束的问题

【算法标签】

线性DP-一维

【代码详解】

#include<iostream>
using namespace std;
const int N = 3005;
int n, h[N], f1[N], f2[N], sf1[N], sf2[N], ans;  // f1: 从左到右的LIS, f2: 从右到左的LIS, sf: 前缀/后缀最大值

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

    // 计算从左到右的最长递增子序列
    for (int i = 1; i <= n; i++)
    {
        f1[i] = 1;
        for (int j = 1; j < i; j++)
        {
            if (h[i] > h[j])
            {
                f1[i] = max(f1[i], f1[j] + 1);
            }
        }
        sf1[i] = max(sf1[i - 1], f1[i]);  // 前缀最大值
    }

    // 计算从右到左的最长递减子序列(从左看是递增)
    for (int i = n; i >= 1; i--)
    {
        f2[i] = 1;
        for (int j = i + 1; j <= n; j++)
        {
            if (h[i] > h[j])
            {
                f2[i] = max(f2[i], f2[j] + 1);
            }
        }
        sf2[i] = max(sf2[i + 1], f2[i]);  // 后缀最大值
    }

    // 计算最长先增后减子序列的长度
    for (int i = 1; i <= n; i++)
    {
        // 情况1:以i为峰值的先增后减序列
        ans = max(ans, f1[i] + f2[i] - 1);
        // 情况2:峰值在i左边和右边的两段独立序列
        ans = max(ans, sf1[i - 1] + sf2[i + 1]);
    }
    // 最少需要移除的人数 = 总人数 - 最长保留序列长度
    cout << n - ans;
    return 0;
}

【运行结果】

5
1 3 2 2 1
1
posted @ 2026-06-14 10:46  团爸讲算法  阅读(19)  评论(0)    收藏  举报