要么做,要么滚!没有试试看这一说

博客引用了《星球大战》中Yoda大师及《生活大爆炸》中Penny的经典台词,强调行动的重要性而非犹豫不决。鼓励读者不要给自己找借口,勇于行动。
词霸每日一句

Do or do not. There is no try.

要么做,要么滚!没有试试看这一说。
感谢wxtsn投稿。词霸小编:这是《星球大战》中Yoda大师说的,而在《生活大爆炸》Penny也说过,而Penny说的口气就不太一样哦!嗯,别为自己找太多理由,行动吧,骚年!
 
原文载于:爱词霸英语学习网-每日一句
涟漪 (rippling.cpp/c) 4.1 问题描述 下雨天,滴水落到水面上,泛起片涟漪。 小可发现,水落到水面上,会使得水面变得高低起伏,非常好看。由此,小可想到,如果个序列也满足这样的规律,那么我们就可以看作是这个序列也泛起了涟漪。 准确来,如果个序列泛起了涟漪,那么除了首尾位置的元素以外,每个位置的元素,要么比两侧相邻的数字小,要么比两侧相邻的数字大。 例如序列 1,3,2,5,3,4 满足这个规律,但是 2,3,4,1,2 不满足,因为第二个位置的元素 比左侧元素 要大,但是要比右侧元素 要小。 注意:如果个序列的长度小于等于 ,那么定是泛起了涟漪。 但是满足这个规律的序列终究是少数,小可想知道,如果现在有个长度为 的序列,每次操作可以将任意个位置的数字修改成任意个新数字。那么想让这个序列泛起涟漪,最最小的修改次数是多少? 4.2 输入格式 输入第行包含个正整数 ,代表数组长度。 接下来行包含 个正整数,其中第 个数字表示数组的第 个元素 。 4.3 输出格式 对于每组询问,输出个整数表示最少修改次数。 4.4 输入样例1 6 1 1 2 2 3 3 4.5 输出样例1 3 4.6 样例1明 可以将序列改为 或者 等,都是可以的。 3 2 4 2 n n n i i ai [1, 5, 2, 4, 1, 3] [1, 0, 2, −1, 5, 3] 703 702 703 703 702 703 702 702 702 700 702 。 。 对于另外 10% 的数据,有 ,且数组元素各不相同。 对于另外 10% 的数据,有 ,且数组元素全部相同。 。 1 ≤ n ≤ 20 1 ≤ n ≤ 1000 1 ≤ n ≤ 105 1 ≤ n ≤ 105 1 ≤ n ≤ 105, 1 ≤ ai ≤ 109
10-25
我们来详细分析并解决这个“涟漪”问题。 --- ### ✅ 问题理解 定义个序列“泛起涟漪”为:**除了首尾元素外,每个中间元素要么是局部极大值(峰),要么是局部极小值(谷)**。 即对于所有 $ 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; } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值