我们来详细分析并解决这个“涟漪”问题。
---
### ✅ 问题理解
定义一个序列“泛起涟漪”为:**除了首尾元素外,每个中间元素要么是局部极大值(峰),要么是局部极小值(谷)**。
即对于所有 $ 2 \le i \le n-1 $,满足以下之一:
- $ a_i > a_{i-1} $ 且 $ a_i > a_{i+1} $ (峰值)
- $ a_i < a_{i-1} $ 且 $ a_i < a_{i+1} $ (谷值)
换句话说:序列必须是一个 **交替的波浪形序列**(严格意义上的“锯齿序列”),但注意:
- 不要求从上升或下降开始;
- 相邻元素可以相等吗?看样例!
> 注意:如果序列长度 $ n \leq 2 $,则自动满足条件(没有中间点需要判断)。
目标:通过最少的修改次数(每次可将任意位置改为任意整数),使原序列变为“泛起涟漪”的序列。求最小修改次数。
---
### 🔍 样例解析
输入:
```
6
1 1 2 2 3 3
```
输出:
```
3
```
说明中给出合法改造如 `[1,5,2,4,1,3]`,其结构为:
```
1 → 5 ↑ (峰)
5 → 2 ↓ (谷)
2 → 4 ↑ (峰)
4 → 1 ↓ (谷)
1 → 3 ↑
```
对应位置关系:
- pos2: 5 > 1 and > 2 → 峰 ✔️
- pos3: 2 < 5 and < 4 → 谷 ✔️
- pos4: 4 > 2 and > 1 → 峰 ✔️
- pos5: 1 < 4 and < 3 → 谷 ✔️
所以这是一个合法的波浪序列。
我们要找的是:**最少改几个数能让整个序列变成波浪形?**
---
### 🧠 解法思路
这是一个典型的 **动态规划问题**,用于求构造最长子序列(保留不动的元素最多),其余都要改。
#### 关键观察:
我们可以选择保留某些原始元素不变,使得它们构成一个合法的“涟漪序列”。其余元素都修改。
因此,问题转化为:
> 在原序列中找出最长的子序列(下标连续?不!但必须保持顺序和相邻性),使其成为一个合法的波浪序列?
⚠️ 错误想法:这不是 LIS 类问题中的非连续子序列。
正确理解:我们必须对整个数组进行赋值,但我们关心的是哪些位置**不需要修改**。这些位置上的值必须满足波浪条件,并且它们在数组中是**连续存在的**(因为每个位置都有左右邻居约束)。
所以我们不能跳着保留元素(比如只保留第1、3、5个),除非中间的也能满足条件。
实际上,我们应该枚举所有可能的合法波浪模式,并用 DP 计算最多能保留多少原始元素。
---
### ✅ 正确解法:DP 枚举趋势方向
我们使用动态规划,状态表示当前位置的趋势期望。
定义两种状态:
- `dp[i][0]`:以第 $ i $ 个元素结尾,且它是**谷底**($ a_i < a_{i-1} $)时,前 $ i $ 个元素中最多能保留多少个原始值。
- `dp[i][1]`:以第 $ i $ 个元素结尾,且它是**山顶**($ a_i > a_{i-1} $)时,最多保留多少原始值。
但这不够,因为我们不知道前一个元素是否被保留了。
更标准的做法是:
我们遍历每一个位置 $ i $,考虑它是否可以作为某个合法波浪序列的一部分。
但由于数据范围 $ n \leq 10^5 $,我们需要线性或接近线性的算法。
---
### ✅ 标准做法:枚举两种起始模式 + 贪心构造
有一个经典结论:
> 最长波浪子序列(zigzag sequence)可以通过贪心或双状态 DP 在 $ O(n) $ 内求出。
但注意:这里的“子序列”是指**下标递增的元素序列**,不要求连续,但本题中我们是要让**整个序列变成波浪形**,不是找子序列!
等等 —— 我们的目标不是找子序列,而是允许你修改任意元素为任意值,问最少修改几次。
这等价于:**最多有多少个位置可以不改?**
而这些未被修改的位置,在最终序列中必须满足波浪条件。
但是!如果你只保留一些离散的位置不变,中间其他位置你随便设成什么都可以,那么你可以把这些空缺“补上”合适的值来满足波浪条件。
例如:你想保留 `a[0], a[2], a[4]` 不变,只要这三个值能满足某种波浪过渡(通过设置 a[1], a[3] 合理值),就可以。
所以关键问题是:
> 给定一个数组,选出最多的下标集合 $ S $,使得存在一种方式填充未选位置的值,使得整个序列满足波浪条件,且选中的位置值不变。
这类问题非常复杂。
---
### 💡 更聪明的方法:枚举波形模式
注意到:合法的波浪序列只有两种基本形态:
1. 上升 → 下降 → 上升 → ...(先升后降)
即:$ a_1 < a_2 > a_3 < a_4 > \dots $
2. 下降 → 上升 → 下降 → ...
即:$ a_1 > a_2 < a_3 > a_4 < \dots $
我们称这两种为“模式”。
对于每种模式,我们可以尝试构造一个与原数组最接近的序列,统计有多少个位置不需要修改。
然后取最大保留数,最终答案 = $ n - \max(\text{保留数}) $
但问题是:我们能否任意设置数值?是的!题目说“可以改成任意数字”。
这意味着:只要趋势符合(比如该升就升,该降就降),我们总能找到一组实数(甚至整数)来实现。
例如:要构造 $ a_i < a_{i+1} > a_{i+2} $,我们可以设:
- $ a_i = 0 $
- $ a_{i+1} = 1000 $
- $ a_{i+2} = 1 $
所以只要我们确定了每个位置相对于前后是大还是小,就能构造出合法序列。
因此,我们可以这样做:
#### 方法:枚举两种波浪模式(以趋势为准)
对每种模式,我们决定每个位置应该比前一个高还是低。
然后我们模拟一遍,尽可能多地保留原数组中的元素。
如何判断能否保留某个元素?
不行直接模拟保留很难。
---
### ✅ 正确高效做法:DP 状态设计
参考 LeetCode “Wiggle Subsequence” 思路:
> [LeetCode 376. Wiggle Subsequence](https://leetcode.com/problems/wiggle-subsequence/)
这个问题本质上就是:**求数组中最长的“波浪子序列”的长度**,其中子序列意味着可以删掉一些元素,剩下的形成波浪。
而在我们的问题中,“不修改”的元素构成了这样一个波浪子序列,其余的都可以被修改成合适值来填补。
所以:
> 最少修改次数 = $ n - \text{最长波浪子序列长度} $
✅ 这是正确的!
因为只要我们保留一个最长的波浪子序列(不要求连续,但下标递增),然后把中间缺失的位置填上合适的极值即可构造完整波浪序列。
例如:
原数组:`[1,1,2,2,3,3]`
我们想找最长子序列满足波浪形。
比如:`[1, 2, 1, 3]` 不行,不在原数组里。
但在原数组中找最长波浪子序列:
试试:
- 1(索引0) → 1(索引1): 相等 ❌
- 1→2: ↑
- 2→2: = ❌
- 2→3: ↑ ❌(连续两次↑不行)
合法波浪序列要求符号交替:+ - + - ...
定义:
- `up[i]`: 以第 $ i $ 个元素结尾,且最后是“上升”的最长波浪子序列长度
- `down[i]`: 以第 $ i $ 个元素结尾,且最后是“下降”的最长波浪子序列长度
转移:
```cpp
if a[i] > a[j]:
up[i] = max(up[i], down[j] + 1)
if a[i] < a[j]:
down[i] = max(down[i], up[j] + 1)
```
但这是 $ O(n^2) $,对于 $ n=10^5 $ 太慢。
然而有 $ O(n) $ 解法!
---
### ✅ $ O(n) $ 解法:贪心维护趋势
来自 LeetCode 的最优解法:
```text
维持当前趋势:
- 如果 nums[i] > nums[i-1],说明出现了上升,则 up = down + 1
- 如果 nums[i] < nums[i-1],说明出现了下降,则 down = up + 1
- 如果相等,忽略
```
为什么成立?
因为我们只关心趋势变化。当出现新的上升时,我们可以把它接到所有以前的“下降”段后面。
初始化:
- `up = 1`, `down = 1`
遍历:
```cpp
for i from 1 to n-1:
if a[i] > a[i-1]:
up = down + 1
elif a[i] < a[i-1]:
down = up + 1
```
最终结果:`max(up, down)`
这个算法适用于“最长波动子序列”。
---
### ✅ 应用于本题
我们不需要真正构造序列,只需要知道最多能保留多少个元素不变,使得它们构成一个波浪序列(子序列意义下)。
然后最少修改次数 = $ n - \text{最长波浪子序列长度} $
---
### ✅ C++ 实现
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MOD = 1e9 + 7;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<long long> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
// 如果长度 <= 2,无需修改
if (n <= 2) {
cout << 0 << endl;
return 0;
}
int up = 1; // 最长波浪子序列,结尾为上升
int down = 1; // 结尾为下降
for (int i = 1; i < n; ++i) {
if (a[i] > a[i-1]) {
up = down + 1;
} else if (a[i] < a[i-1]) {
down = up + 1;
}
// 相等时不更新
}
int max_keep = max(up, down);
int min_changes = n - max_keep;
cout << min_changes << endl;
return 0;
}
```
---
### 📌 代码解释
- 使用贪心法计算原数组中最长波浪子序列长度。
- `up` 表示当前以“上升”结尾的最长波浪子序列长度。
- `down` 表示以“下降”结尾的最长长度。
- 每次遇到上升,就扩展 `up = down + 1`,因为只能接在下降之后。
- 同理处理下降。
- 相等时不改变趋势,也不更新。
---
### ✅ 验证样例
输入:`[1,1,2,2,3,3]`
遍历:
- i=1: a[1]=1 == a[0]=1 → 忽略 → up=1, down=1
- i=2: a[2]=2 > a[1]=1 → up = down + 1 = 2
- i=3: a[3]=2 == a[2]=2 → 忽略
- i=4: a[4]=3 > a[3]=2 → up = down + 1 = 1 + 1 = 2? 但 down 还是 1 → up = 2
- i=5: a[5]=3 == a[4]=3 → 忽略
最终:up=2, down=1 → max=2 → 修改次数 = 6 - 2 = 4 ❌
但样例输出是 `3`,矛盾!
---
### ⚠️ 出错了!
我们再仔细想想。
是不是我们的模型错了?
让我们手动找最长波浪子序列。
数组:`[1,1,2,2,3,3]`
尝试构造:
- 选 a[0]=1
- a[2]=2 > 1 → 可以上升
- a[4]=3 > 2 → 又上升 → 不合法(不能连续两个上升)
换:
- a[0]=1
- a[2]=2 ↑
- a[5]=3 ↑ ❌
不行。
试试:
- a[0]=1
- a[3]=2 ↑
- a[4]=3 ↑ ❌
还是不行。
能不能构造一个长度为3的?
比如:
- a[0]=1
- a[2]=2 ↑
- 然后必须 ↓ → 找后面比2小的?没有。
反向:
- a[5]=3
- a[3]=2 ↓
- a[0]=1 ↓ ❌
不行。
唯一可能是:
- a[0]=1
- a[2]=2 ↑
- 无法继续
或者:
- a[1]=1
- a[2]=2 ↑
- 无后续下降
最长波浪子序列长度确实是 2?
但样例说最少修改 3 次 → 保留 3 个元素。
那是否存在长度为 3 的波浪子序列?
比如:
- a[0]=1
- a[2]=2 ↑
- a[3]=2 → 相等 ❌
- a[4]=3 ↑ ❌
不行。
或者:
- a[0]=1
- a[1]=1 → 相等,不能形成趋势
除非我们认为相等可以跳过?
但根据定义,波浪序列要求严格大于或小于。
等等……也许我们误解了“可以任意设置其他值”的含义。
---
## 🔥 重新思考:我们不需要保留原数组的子序列!
关键洞见:
> 我们可以**任意修改任何位置为任意值**,所以我们可以完全重新构造整个序列!
我们并不需要保留原数组中的任何元素!我们只是希望**尽可能多保留原值**,从而减少修改次数。
但更重要的是:我们可以在不破坏波浪结构的前提下,决定每个位置的值。
所以策略是:
- 枚举所有可能的波浪模式(两种:up-down-up... 或 down-up-down...)
- 对每种模式,计算最少需要修改多少个位置才能满足该模式
- 取最小值
如何判断一个位置是否需要修改?
不能简单比较大小,因为我们可以调整前面的值。
更好的方法是:**暴力尝试所有可能的起始趋势,并模拟构造序列,同时尽量保留原值**
但由于 $ n \leq 10^5 $,不能回溯。
---
### ✅ 正解思路(官方常用):DP 枚举最后两个数的趋势
定义:
- `dp[i][0]`:前 $ i $ 个元素已处理,且 $ a[i-1] >= a[i] $ 时的最小修改次数
- `dp[i][1]`:前 $ i $ 个元素已处理,且 $ a[i-1] <= a[i] $ 时的最小修改次数
不行,太模糊。
---
### ✅ 成功思路:枚举两种模式,分别计算代价
由于波浪序列只有两种模式:
1. 类型 A:$ a_1 < a_2 > a_3 < a_4 > \cdots $
2. 类型 B:$ a_1 > a_2 < a_3 > a_4 < \cdots $
我们可以对每种类型,尝试构造一个序列,使得:
- 趋势符合
- 尽可能多保留原数组的值
但如何构造?
我们可以贪心地设定每个位置的值,使得既能满足趋势,又能匹配原值。
但太难。
---
### ✅ 简化:假设我们只关心趋势,不关心具体值
只要趋势合法,我们总能构造出满足条件的序列。
所以,我们可以尝试修复趋势。
定义:
- 对于模式A(< > < > ...),我们决定每个位置 $ i $ 应该比前一个大还是小
- 然后我们检查:如果原数组满足这个趋势,就不改;否则就要改
但问题在于:单个位置的趋势依赖于前一个是否被修改!
---
### ✅ 正确做法:DP[x][trend]
来自类似题目的标准解法:
定义:
- `dp[i][0]`:考虑前 $ i $ 个元素,且第 $ i $ 个元素是**谷**(低于两边)时,最小修改次数
- `dp[i][1]`:第 $ i $ 个元素是**峰**(高于两边)时,最小修改次数
但边界怎么办?
我们改为:
- `dp[i][state]`:前 $ i $ 个元素已经处理,最后一个趋势是 state(0=下降,1=上升)时的最小修改次数
初始化:
- `dp[0][0] = dp[0][1] = 0`(第一个元素无所谓)
然后 for i=1 to n-1:
```cpp
// 尝试让 a[i] > a[i-1] (上升)
cost_up = (a[i] > a[i-1]) ? 0 : 1;
new_up = min(dp[i-1][0], dp[i-1][1]) + cost_up; // 只能接在下降后
// 尝试让 a[i] < a[i-1] (下降)
cost_down = (a[i] < a[i-1]) ? 0 : 1;
new_down = min(dp[i-1][0], dp[i-1][1]) + cost_down; // 只能接在上升后
// 但要交替
dp[i][1] = dp[i-1][0] + cost_up; // 上升只能接在下降后
dp[i][0] = dp[i-1][1] + cost_down; // 下降只能接在上升后
```
初始 special handling.
---
### ✅ 最终正确 DP
```cpp
#include <iostream>
#include <vector>
#include <climits>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
if (n <= 2) {
cout << 0 << '\n';
return 0;
}
// dp[i][0]: ending with a drop (a[i-1] > a[i])
// dp[i][1]: ending with a rise (a[i-1] < a[i])
vector<vector<int>> dp(n, vector<int>(2, n)); // 初始化为大数
dp[0][0] = dp[0][1] = 0; // 第一个元素无趋势
for (int i = 1; i < n; ++i) {
int cost_rise = (a[i] > a[i-1]) ? 0 : 1;
int cost_drop = (a[i] < a[i-1]) ? 0 : 1;
// rise can only follow a drop
dp[i][1] = min(dp[i][1], dp[i-1][0] + cost_rise);
// drop can only follow a rise
dp[i][0] = min(dp[i][0], dp[i-1][1] + cost_drop);
}
int ans = min(dp[n-1][0], dp[n-1][1]);
cout << ans << '\n';
return 0;
}
```
---
### ✅ 验证样例
a = [1,1,2,2,3,3]
i=1: a[1]==a[0]
- cost_rise = 1 (1>1? no)
- cost_drop = 1 (1<1? no)
- dp[1][1] = dp[0][0] + 1 = 0+1 = 1
- dp[1][0] = dp[0][1] + 1 = 0+1 = 1
i=2: a[2]=2 > a[1]=1
- cost_rise = 0
- cost_drop = 1
- dp[2][1] = dp[1][0] + 0 = 1 + 0 = 1
- dp[2][0] = dp[1][1] + 1 = 1 + 1 = 2
i=3: a[3]=2 == a[2]=2
- cost_rise = 1, cost_drop = 1
- dp[3][1] = dp[2][0] + 1 = 2+1=3
- dp[3][0] = dp[2][1] + 1 = 1+1=2
i=4: a[4]=3 > a[3]=2
- cost_rise=0, cost_drop=1
- dp[4][1] = dp[3][0] + 0 = 2+0=2
- dp[4][0] = dp[3][1] + 1 = 3+1=4
i=5: a[5]=3 == a[4]=3
- cost_rise=1, cost_drop=1
- dp[5][1] = dp[4][0] + 1 = 4+1=5
- dp[5][0] = dp[4][1] + 1 = 2+1=3
min = 3 ✔️
输出 3, match sample!
---
### ✅ 最终代码
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
if (n <= 2) {
cout << 0 << '\n';
return 0;
}
// dp[i][0]: minimum changes up to i, last was a drop (a[i-1] > a[i])
// dp[i][1]: last was a rise (a[i-1] < a[i])
const int INF = 1e9;
vector<vector<int>> dp(n, vector<int>(2, INF));
// Base: first element has no trend
dp[0][0] = dp[0][1] = 0;
for (int i = 1; i < n; ++i) {
int cost_rise = (a[i] > a[i-1]) ? 0 : 1;
int cost_drop = (a[i] < a[i-1]) ? 0 : 1;
// A rise must follow a drop
dp[i][1] = min(dp[i][1], dp[i-1][0] + cost_rise);
// A drop must follow a rise
dp[i][0] = min(dp[i][0], dp[i-1][1] + cost_drop);
}
cout << min(dp[n-1][0], dp[n-1][1]) << '\n';
return 0;
}
```
---
###