题解:学而思编程 改造过山车
【题目来源】
学而思编程:改造过山车
【题目描述】
游乐园的老板要改造他的过山车,小明接到任务,要对过山车的高度进行改造。过山车分为 \(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
【核心思想】
-
问题分析:给定 \(n\) 段过山车的高度 \(h_i\),需要改造成先严格上升再严格下降的形式(山峰形状),求最少需要重新安装多少支柱。等价于求最长的先增后减子序列(山峰型子序列),最少需要移除的支柱数 = \(n\) - 最长保留序列长度。这是一个双向LIS问题。
-
算法选择:
- 正向LIS:
f1[i]表示从左到右,以第 \(i\) 个支柱结尾的最长递增子序列长度 - 反向LIS:
f2[i]表示从右到左,以第 \(i\) 个支柱结尾的最长递增子序列(即原序列中以 \(i\) 为起点向右的最长递减子序列) - 前缀/后缀最大值:
sf1[i]表示前 \(i\) 个位置的最长递增子序列长度,sf2[i]表示从第 \(i\) 个位置到末尾的最长递减子序列长度
- 正向LIS:
-
关键步骤:
- 计算正向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]
- 以 \(i\) 为峰值:
- 输出答案:
n - ans,其中ans是最长保留序列长度
- 计算正向LIS:遍历 \(i\) 从 \(1\) 到 \(n\),对于每个 \(i\),遍历 \(j\) 从 \(1\) 到 \(i-1\),若 \(h[i] > h[j]\),则
-
时间/空间复杂度:
- 时间复杂度:\(O(n^2)\),两次LIS计算,每次 \(O(n^2)\)
- 空间复杂度:\(O(n)\),四个数组
f1、f2、sf1、sf2
-
双向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
浙公网安备 33010602011771号