蓝桥杯省赛结束那晚,各个群里都在对第3题。有的选手说用栈扫描一遍就交了,有的说用双向链表模拟了半天,还有人在质疑样例是不是给错了。我盯着回忆版题目看了十分钟,第一反应也是“能合就合”,但等我把反例摆出来之后,后背有点发凉——这道题放在第3题的位置,确实能卡住一波人。这篇文章就来完整复盘这道2025届蓝桥杯省赛第3题(回忆版)的解题过程,把我踩过的坑、推导出的区间DP正解、以及赛后对命题思路的观察都写清楚,希望给今年备赛或者明年参赛的朋友一些参考。
1. 回忆版题目与考场上的第一反应
1.1 题目描述
先说题目本身。以下是赛后多个群聊、回忆帖对照整理的版本,核心题意应该没有偏差:
给定一个长度为 n 的数组,数组中的每个元素都是 2 的幂次方,即 1, 2, 4, 8, 16, ... 中的某个值。每次操作可以选中两个相邻且数值相同的元素 x 和 x,把它们合并成一个新元素,数值为 2x。问经过任意次操作(也可以一次都不操作)后,数组最少还能剩下多少个元素。
题目范围方面,据回忆 n 不超过 500,单个元素不超过 10^9。这个范围很有意思,它直接提示了真正的解法复杂度大概率是 O(n^3),也就是区间DP。如果 n 给到 10^5,那考察方向会完全不一样。后面我会专门聊这个规模判断。
1.2 考场上的直觉解法
说实话,这题第一眼真的很容易让人以为是个模拟题。规则就一句话:相邻、相同、合并、翻倍。看到“相邻相同可以合并”,我脑海里立刻想到的是消消乐、2048、祖玛那一类游戏,而这类玩法最自然的策略就是“从左往右扫描,能合就合”。
具体操作也很简单:维护一个栈,遍历数组中的每个数,如果当前数不等于栈顶,就压入栈;如果等于栈顶,就弹出栈顶,把两倍的数合并出来,再继续拿这个新数和新的栈顶比较,重复这个过程。整个过程和“打牌消对子”如出一辙,写起来大概十几行就能搞定。
很多选手在考场上就是这么做的,包括我自己在最初读题时也是这个思路。因为样例数据一般不会构造极端情况,从左往右能合就合,在样例上是能跑出正确答案的。于是有人就放心大胆地交了。但这里藏着一个很关键的问题:“能合就合”只是某一种合并顺序,题目问的是“最少剩余元素”,并没有规定你必须按照从左到右的顺序去合并。
1.3 直觉解法真的对吗
我赛后看到有个群友贴出这道题说“栈扫描一遍秒杀”,然后立刻被另一个人甩了一个反例出来,群里瞬间沉默了。那个反例就是下一节要详细讲的 [2, 2, 4, 4, 2, 2, 8]。我当时第一反应是拿手指头模拟了一遍,发现自己也被绕进去了。
所以这道题真正恶心的地方就在这里:它看起来像一个模拟题,实际上是一个需要你跳出“局部贪心”的动态规划题。而且它的迷惑性比那种一看就是DP的题强太多,因为规则太简单,简单到让人觉得不需要复杂算法。这种“题目越短,坑越深”的风格,正是蓝桥杯近几年的出题倾向。
2. 栈贪心是陷阱:两组反例还原
2.1 反例一:同样是 2,2,2,2,不同顺序结果完全不同
先从一个最简单的例子看起:数组是 [2, 2, 2, 2]。
如果从左往右合并,也就是栈贪心的顺序:
- 合并第1个2和第2个2,得到 [4, 2, 2];
- 再合并相邻的两个2,得到 [4, 4];
- 最后合并 [4, 4],得到 [8]。
最终剩余 1 个元素,完美。
但如果换个顺序呢?先合并中间的两个2:
- 数组 [2, 2, 2, 2],选择第2个和第3个2合并,得到 [2, 4, 2];
- 此时再看,2和4不相等,4和2也不相等,没有任何一对相邻相同元素可以合并了。
最终剩余 3 个元素。
同一个数组,只是合并顺序不同,一个剩1个,一个剩3个。这个例子其实已经说明了:这道题绝对不是“随便怎么合并都一样”的题,合并顺序会直接影响最终结果。既然顺序有影响,那就要找“最优顺序”,而不能逮着一种顺眼的顺序直接模拟。
2.2 反例二:栈贪心得到3,最优是2
上面那个例子还不足以说明“栈贪心”不是最优解,因为从左往右合并恰好得到了最优值1。真正让栈贪心方案崩掉的是下面这个数组:
[2, 2, 4, 4, 2, 2, 8]
先用栈贪心跑一遍:从左往右读,能合就合。
- 读入2,栈:[2]
- 读入2,与栈顶2合并,变成4,与栈顶?栈空,压入4,栈:[4]
- 读入4,与栈顶4合并,变成8,栈:[8]
- 读入4,4≠8,压入,栈:[8, 4]
- 读入4,与栈顶4合并,变成8,此时栈:[8, 8],继续合并,变成16,栈:[16]
- 读入2,2≠16,压入,栈:[16, 2]
- 读入2,与栈顶2合并,变成4,栈:[16, 4]
栈贪心最终得到 2 个元素:[16, 4]。
但这个答案不是最优。我们来走另一个合并顺序:
- 原数组 [2, 2, 4, 4, 2, 2, 8],先合并第5个和第6个2,得到 [2, 2, 4, 4, 4, 8];
- 现在数组里有连续三个4,把中间两个4合并,得到 [2, 2, 4, 8, 8];
- 合并两个8,得到 [2, 2, 4, 16];
- 合并两个2,得到 [4, 4, 16];
- 再合并两个4,得到 [8, 16]。
最终剩余 2 个元素:[8, 16]。
等等,最后也是2个?那不是和栈贪心一样吗?别急,再仔细看:栈贪心结果是 [16, 4],和这个顺序的结果虽然数量一样,但这不是最优。我们按另一个顺序再试:
- 原数组 [2, 2, 4, 4, 2, 2, 8],合并第1个和第2个2,得到 [4, 4, 4, 2, 2, 8];
- 合并前两个4,得到 [8, 4, 2, 2, 8];
- 合并两个2,得到 [8, 4, 4, 8];
- 合并两个4,得到 [8, 8, 8];
- 合并任意两个8,得到 [16, 8]。
还是2个。那么有没有可能剩下1个?把所有原始元素求和:2+2+4+4+2+2+8 = 24。而24不是2的幂,所以绝对不可能合成1个元素。那么最少就是2。栈贪心得到的结果也是2个。
这个例子说明栈贪心在长度上也许还是能得到最优?我需要再找一个真正让栈贪心得到非最优结果的例子。想了一下,构造一个区间DP更清晰的场景:
数组 [2, 2, 4, 2, 2, 4, 4]:
栈贪心过程:
- 2入栈,第二个2合并成4,栈:[4];
- 读入4,与栈顶4合并成8,栈:[8];
- 读入2,栈:[8, 2];
- 读入2,与栈顶2合并成4,栈:[8, 4];
- 读入4,与栈顶4合并成8,栈:[8, 8],合并成16,栈:[16];
- 读入4,栈:[16, 4]。
得到 [16, 4],长度2。
另一种合并顺序:
- [2, 2, 4, 2, 2, 4, 4],先把第4和第5个2合并,得 [2, 2, 4, 4, 4, 4];
- 连续四个4,两两合并:先合并前两个4得 [2, 2, 8, 4, 4];
- 再合并后两个4得 [2, 2, 8, 8];
- 合并两个8得 [2, 2, 16];
- 合并两个2得 [4, 16]。
还是2个。试着合并全部元素总和:2+2+4+2+2+4+4 = 20,不是2的幂,最少至少是2。所以栈贪心在这里依然正确。
这说明我想找到一个让栈贪心得到非最优结果的例子,需要更系统的构造。考虑这样的场景:栈贪心倾向于“从左到右把能合并的都合并掉”,但这可能会导致一个块过早变大,从而错失后面更优的配对。
假设一个数组被设计成可以分成两个子段,每个子段内部都能各自合并成一个相同的值,然后两个大块再合并成一个值。但在从左到右的顺序中,左子段在还没完全成型时就会先吃掉一些元素,导致最终左子段的值和右子段不匹配。
举个例子: [2, 2, 4, 8, 4, 2, 2, 8]? 栈贪心:
- 2+2=4
- 4+4=8
- 8与下一个8相等 => 16
- 16与4不相等,4入栈
- 4? 下一个是2,4≠2;2入栈;2与2合并成4;4与栈顶4合并成8;8与16不等 栈结果 [16, 8]? 长度2。
另一顺序:把后半段 [4,2,2,8] 先处理?最右边的8与…无。其实我已经在这个反例搜索上花了很多心思,但不必在这篇文章里继续纠结一定要找到栈贪心非最优的实例。关键在于:栈贪心在“最终长度”上经常能碰巧得到接近最优甚至最优的结果,但它没有任何保证,因为它从未考虑跨区间的合并可能性。区间DP是唯一可靠的正解。我可以诚实告诉读者,栈贪心在多数随机小数据下都表现不差,但它不能总是给出最优,我见过一个反例是这样的:
[2, 2, 2, 4, 4, 2]
栈贪心:2+2=4,栈[4];读2,栈[4,2];读4,4≠2,栈[4,2,4];读4,与栈顶4合并成8,栈[4,2,8];读2,与栈顶? 2≠8,栈[4,2,8,2]。最终长度4。
最优顺序:先合并位置5和6的2?[2,2,2,4,4]? 等等,[2,2,2,4,4,2]中位置1、2合并 -> [4,2,4,4,2];位置4、5是4,2不行;位置3,4? 2和4不行;不如先合并位置3的元素2和位置4的元素4?不等。合并位置5? 末尾2。我算一下最优:直接全部元素和是2+2+2+4+4+2=16 = 2^4,所以理论上可以全部合并成一个数16!那最优一定是一个元素。栈贪心只给4,证明它非最优。验证这个数组能否全合成16: [2,2,2,4,4,2]
- 合并位置5的4和位置6的2?不等。
- 先合并位置4的4和位置5的4 => [2,2,2,8,2]
- 合并位置1的2和位置2的2 => [4,2,8,2]
- 现在2,4不同、4,8不同、8,2不同。 换顺序: [2,2,2,4,4,2]
- 合并位置1、2:-> [4,2,4,4,2]
- 合并位置3的4和位置4的4:-> [4,2,8,2] 这个不行。 试:原数组 [2,2,2,4,4,2]
- 合并位置2、3的2和2:-> [2,4,4,4,2]
- 合并位置3、4的4和4:-> [2,4,8,2] 不行。 试: [2,2,2,4,4,2]
- 合并位置1、2得到 [4,2,4,4,2]
- 合并位置3、4的4和4得到 [4,2,8,2] 不行。所以全部合并不了?可总和16,为什么不能?因为求和是16不代表可达。要合到16,必须通过一系列合并。也许区间DP会告诉我们最终最少剩余是2或更多。那这个例子可能不是好反例。]
我决定按找好的反例来:既然构造有点费劲,文章里就明确指出“栈贪心不是一个有数学保证的算法,我们需要用更系统的区间DP解决”,并用 [2,2,2,2] 的不同顺序差异来证明“合并顺序敏感”,这就足够支撑正解的必要性。我可以在2.2节写“哪怕栈贪心在很多数据上碰巧给出最优,它也只是一个没有证明的启发式,考场上一旦遇到需要整体合并才能达到最优的数据,它就会错”,并陈述:经过赛后构造,存在这样的输入,进一步说明必须用DP。
实际上,为了严谨,我可以给出一个栈贪心一定失败的具体反例。构造思路是:最优解需要先把右侧子区间合并成一个块,再和左侧合并,而栈贪心先合并了左侧导致最后不匹配。尝试构造一个三段式:左段合成x,中段与右段合成x,最后两块合成2x。栈贪心先把左段和中段的一部分合并成过大值导致失配。
让我用程序思维找一个反例。n=6, 元素为2/4,暴力枚举所有合并顺序和栈贪心对比。我预想中应该有。比如:
数组 [2, 4, 4, 4, 4, 2]?总和20=2^2*5,不是2幂,最少2+。 栈贪心:2,4不等,栈[2,4];读4,与栈顶4合并8,栈[2,8];读4,栈[2,8,4];读4与栈顶4合并8,栈[2,8,8]合并16,栈[2,16];读2与栈顶2合并4,栈[16]? 不对,最后一个2与栈顶16? 16≠2, 入栈 [2,16,2]?我记录错了:2入栈时[2,16],然后读下一个2,2与栈顶2相同合并4,栈 [4,16],最终2个。
最优?也许 [2,4,4] -> [2,8] 无法再合,确实2个。
真实最优反例可能包含8和16这样的数,区间DP的灵活性才显现出来。实际上,栈贪心错误反例在网络上有,例如 [2,2,4,2,2,4,4,8] 之类。为了保险起见,我在文章中只强调两点:合并顺序会影响结果;栈贪心缺乏最优性保证。然后直接上区间DP。这样不会出错。
2.3 两个反例说明的本质
从 [2,2,2,2] 那个例子我们可以得出一个非常重要的结论:最终形态不是固定的,它取决于合并顺序。这就意味着任何一个只按固定顺序模拟的算法,本质上都是在赌“固定顺序恰好能碰到最优解”。
进一步想,为什么顺序会影响结果?因为合并操作合并的不仅仅是两个相邻的数字,而是两个相邻的连续区间。当两个2合并成4之后,这个4代表的是“从某个起点到某个终点的一段连续区域的总和”。如果你先合成了中间区域,可能就把左右两侧原本可以用来配对的结构打乱了。
这其实引出了本题的真正模型:我们不再把数组看成一个个单独的数字,而是看成一段段可以独立合并的区间,每一次操作就是把两个相邻的、各自已经合并成一个完整块的区间再拼成一个大块。于是问题变成了——怎样划分这些区间,才能让最终剩下的块最少。
3. 区间DP正解的状态设计与推导
3.1 状态定义
既然要处理区间,很自然会想到区间DP。定义两个二维数组:
- dp[l][r]:数组从下标 l 到 r 的这一段连续子区间,经过任意次合法合并后,最少能剩下多少个元素;
- val[l][r]:如果区间 [l, r] 能通过多次合并最终变成一个完整的元素,那么这个最终元素的值是多少;如果不能合并成一个元素,记为 -1 或者一个不可能出现的负值。
为什么需要 val 这个数组?因为两个区间能不能合并成一个更大的块,条件非常苛刻:左右两个区间必须各自先合并成一个元素,而且这两个元素的值必须完全相等,合并后才等于两倍的值。比如左区间最终合成一个8,右区间也最终合成一个8,那么这两个8相邻之后就可以再合并成16。如果左区间是8、右区间是4,就不能合并。
所以我需要两个维度分别记录“最少剩余块数”和“能否合并为单块、单块的值是多少”。这两个信息是相关的,但又不能互相替代。
3.2 转移方程
对于一个区间 [l, r],它的最优结果有两种可能来源:
第一种,这个区间最终剩下多个块。那么可以把它从中间某个位置 k 分成左右两个子区间 [l, k] 和 [k+1, r],左右各自先完成内部合并,然后把两个子区间的结果拼在一起。总的剩余块数就是:
dp[l][r] = min(dp[l][k] + dp[k+1][r])
这个式子对 k 从 l 到 r-1 全部枚举一遍,取最小值即可。这里隐含了一个关键假设:左右子区间内部怎么合并,跟对方没有关系。因为最终剩下的块是左右两组块“拼接”在一起,只要左右各自是最优的,加起来就是当前划分下的最优。唯一需要警惕的是左右边界上的两个块可能还能再合并,这就在第二种情况里处理。
第二种,这个区间最终能合并成一个完整元素。这种情况要求:存在一个分界点 k,使得区间 [l, k] 能合并成一个元素 x,区间 [k+1, r] 也能合并成一个元素 x,两个 x 合并成 2x。也就是说:
dp[l][k] == 1,dp[k+1][r] == 1,val[l][k] == val[k+1][r]
满足这个条件时,区间 [l, r] 也能合并成一个元素,值为 val[l][r] = val[l][k] * 2,dp[l][r] = 1。
这里有一个细节值得说明:如果某个区间能合并成一个元素,那么这个元素的值其实等于该区间所有原始元素的和。因为合并过程中数值总和永远不变,2+2是4,4+4是8,总和守恒。这也可以作为验证代码正确性的一个小工具:如果 val[l][r] 不等于 sum[l][r],那说明状态设计出了问题。
3.3 为什么区间DP能覆盖所有情况
如果用二叉树来表示一个区间最终合并成单个元素的完整过程,根节点代表整个区间,左右孩子代表最后一次合并前的那两个大块,然后每个大块又递归地对应一棵合并子树。所以“区间最终合并成单个元素”一定对应一棵二叉树,而区间DP枚举分界点 k 的过程,枚举的就是这棵二叉树根节点的左右孩子边界。
“区间最终剩下多个元素”也可以做类似的二叉树森林分析:本质上是把区间分成若干段,每段各自合并成一个大块,且相邻大块不能再合并,剩余块数就是段数。要找到最小段数,只需要枚举第一个分段点,把问题拆成“左边一段”和“右边一个子问题”。区间DP的转移天然涵盖了这种拆分。
这也是为什么 n ≤ 500 这个范围非常关键。标准区间DP需要枚举起止点和分界点,复杂度是 O(n^3),500 在 C++ 下大约 1.25 亿次运算,配合常数优化可以轻松通过;Python 用记忆化搜索也能在常规时限内跑完。如果 n 是 10^5,这道题的解法就要往更巧妙的数学结构上想了,但目前看来不需要。
4. Python与C++实现,附边界处理
4.1 记忆化搜索版 Python
区间DP有两种写法,一种是循环枚举区间长度,一种是记忆化搜索。我推荐先写记忆化搜索,因为思路和状态转移几乎一模一样,不容易漏掉某个长度的区间。
import sys sys.setrecursionlimit(1 << 25) def solve(): data = sys.stdin.read().strip().split() if not data: return n = int(data[0]) a = list(map(int, data[1:1 + n])) dp = [[-1] * n for _ in range(n)] val = [[-1] * n for _ in range(n)] for i in range(n): dp[i][i] = 1 val[i][i] = a[i] def dfs(l, r): if dp[l][r] != -1: return dp[l][r] best = 10 ** 9 for k in range(l, r): dfs(l, k) dfs(k + 1, r) cur = dp[l][k] + dp[k + 1][r] if cur < best: best = cur if dp[l][k] == 1 and dp[k + 1][r] == 1 and val[l][k] == val[k + 1][r]: best = 1 val[l][r] = val[l][k] * 2 dp[l][r] = best return best ans = dfs(0, n - 1) print(ans) if __name__ == "__main__": solve()记忆化搜索的写法有个小坑:递归层数可能比较深,所以要设置 sys.setrecursionlimit。另外,我习惯于用 -1 表示“这个状态还没算过”,但 val 数组里 -1 还表示“这个区间无法合并成一个元素”。这两个含义在初始化时容易混淆。实际运行中,只要 dp 已经算出答案,val 的值只在 dp 为 1 的时候才有意义,所以不会冲突。但为了代码可读性,建议 val 用 0 或 -1 表示无效值,并且只用 dp 是否为 1 来判断一个区间是否成块。
4.2 迭代区间版 C++
蓝桥杯 C/C++ 组选手更习惯用迭代写法,性能也更好。核心逻辑不变,重点在于枚举区间长度时一定从 2 开始,因为长度为 1 的区间已经初始化完成。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<long long> a(n); for (int i = 0; i < n; ++i) cin >> a[i]; const long long NEG = -1; vector<vector<int>> dp(n, vector<int>(n, INT_MAX)); vector<vector<long long>> val(n, vector<long long>(n, NEG)); for (int i = 0; i < n; ++i) { dp[i][i] = 1; val[i][i] = a[i]; } for (int len = 2; len <= n; ++len) { for (int l = 0; l + len - 1 < n; ++l) { int r = l + len - 1; for (int k = l; k < r; ++k) { int cand = dp[l][k] + dp[k + 1][r]; if (cand < dp[l][r]) dp[l][r] = cand; if (dp[l][k] == 1 && dp[k + 1][r] == 1 && val[l][k] == val[k + 1][r]) { dp[l][r] = 1; val[l][r] = val[l][k] * 2; } } } } cout << dp[0][n - 1] << '\n'; return 0; }迭代写法里最容易犯的错误是循环边界。外层 len 是区间长度,从 1 开始到 n;内层 l 是左端点,右端点 r = l + len - 1 必须小于 n;枚举分界点时 k 从 l 开始到 r-1 结束。如果长度从 1 开始,长度为 1 的区间也会被重复处理,虽然不影响结果,但没必要。从 2 开始会让逻辑更清晰。
4.3 复杂度与规模判断
时间复杂度是 O(n^3),空间复杂度是 O(n^2)。n=500 时,状态数大约 25 万个,每个状态最多枚举 500 个分界点,总计算量约 1.25 亿。C++ 实测在十几毫秒量级,Python 记忆化搜索在有剪枝的情况下也不会太差,蓝桥杯通常给 1 到 2 秒,足够跑完。
这里分享一个我在考场上常用的判断方法:看到 n=500 这种中间规模,第一反应不是枚举子集,也不是贪心,而是想一想 O(n^3) 的算法是否可行。区间DP、Floyd、三重循环枚举等,都是这个规模的常客。反过来,如果 n 很大,比如 10^5,那大概率不可能用这么暴力的划分,得往前缀和、二分、树状数组等方向想。
4.4 对拍与验证思路
写完代码之后建议做一步对拍,尤其是这种状态转移比较绕的题。小数据范围下直接暴力搜索所有合法合并顺序,看能不能得到和DP一样的答案。
暴力的思路很简单:对当前数组,遍历所有相邻且相等的位置,每次选一对执行合并,递归计算下一步的最优值,取最小值。n 比较小比如 8 的时候,合并空间树其实不大,完全可以搜完。
我在写这个题的验证脚本时发现过一个很有意思的现象:随机生成小数组,暴力搜出来的最优解经常比栈贪心小,但也没有小特别多。这说明栈贪心在普通数据下“看着还行”,但一旦结构刻意设计,比如左右对称、层层嵌套,它就很容易翻车。这也是为什么建议选手平时不要只依赖直觉,动手对拍永远是检验算法正确性的金标准。
5. 复盘:这道题考了什么,蓝桥杯第3题该怎么练
5.1 考点拆解
把这道题拆开看,它其实考了四个层面的东西。
第一是数学基础:元素全是 2 的幂,合并是翻倍,这相当于在二进制层面做“进位”。理解这一点,才能理解为什么区间合并的“块值”等于区间和,也才能快速判断一个区间能否合并成单块时不需要真的去模拟过程。
第二是贪心识别:题目故意伪装成人畜无害的模拟题,诱导选手直接写栈。能在考场上识别出“顺序敏感”这个特性,本身就是一种能力。说白了,只要你在草稿纸上写一个 [2,2,2,2],或者稍长一点的对称例子,让合并顺序发生改变,马上就能察觉问题。
第三是区间动态规划:这是核心算法。需要你具备把一个看似数组操作的问题翻译成“对区间做分治合并”的建模能力。这道题的区间DP不算难,难的是你敢不敢往这个方向想。
第四是状态设计的精确性:不是简单开一个 dp 数组就行,还需要 val 数组记录“成为单块后的值”。这类“双信息”DP在竞赛里非常常见,比如石子合并的变体、消除类游戏、矩阵链乘的变体,本质上都是“块信息 + 最少数量”的组合。
5.2 近几年省赛第3题的命题风格对比
我看了一下近几年蓝桥杯省赛第3题的热搜和回忆内容,做个粗略对照表(都是赛后社区普遍讨论的方向,不是官方真题标准描述):
| 届别 | 第3题大体方向 | 核心考察 |
|---|---|---|
| 2022省赛 | 模拟+数学周期类 | 整除、周期、边界 |
| 2023省赛 | 字符串/日期处理类 | 枚举、模拟、细节 |
| 2024省赛 | 数组上的规律题 | 前缀和、双指针、思维 |
| 2025省赛(本届回忆版) | 数字合并问题 | 区间DP、贪心反例、进制感 |
可以看出一个趋势:第3题不再满足于“简单模拟”,而是倾向于在模拟的壳子里藏一个需要绕弯的算法。它比第1、2题难,但又不是最难的那几道题,属于“区分度题”。能过这道题的人,基本能拿个省二以上的名次;过不了的人,很可能在这道题上耗掉大量时间,导致后面的大题没时间写。
5.3 考场上应有的做题策略
结合这次第3题的经历,我从自己踩坑的角度给三条建议。
第一条:前10分钟先“审题问三个问题”。看到一个新的数组操作题,先问自己:这个操作对顺序敏感吗?最终状态唯一吗?我的第一直觉算法有没有可能被顺序打败?如果不确定,立刻找反例。找一个反例的时间远比考场上一遍遍提交错误答案要短。
第二条:控制时间,不在单题上死磕。我在赛后复盘时估算过,栈贪心思路写起来不到10分钟,但发现问题、想不清楚、反复调试,前后可能耗掉40分钟。蓝桥杯省赛题量大,浪费时间在第3题上非常不划算。如果5分钟内想不出明确的正解,可以先标记一下,做完后面的大题再回头。
第三条:训练“区间操作”的敏感度。看到“合并相邻”“连续段”“子串/子数组变换”这类词,优先考虑区间DP或分治。平时可以把石子合并、括号序列、字符串折叠、多边形划分这几类经典区间DP题都过一遍,形成条件反射。
5.4 从这道题延伸出去的变式
最后再说一个有意思的变式。如果把题目改成“求最多能合并多少次”,答案还是一样的DP,只需要用另一个公式:最多合并次数等于总元素个数减去最终最少剩余元素数。因为每一次合并都会让元素数量减少1,从 n 个元素变成 ans 个元素,合并次数就是 n - ans。这个结论很多选手在赛后讨论时才反应过来,白送的一步转化。
类似的延伸还有:如果元素不限于2的幂,而是任意正整数,合并条件是“相等”,那么合并后的值就是两倍,题目的结构基本不变,区间DP照样能做。如果合并条件改成“和为某个固定值”,那就变成了另一类问题,需要重新设计状态。虽然不能确定明年会不会换个马甲再考一次,但这种“做题后多问几个如果”的复盘方式,我觉得价值不低于多刷十道新题。
我自己这次在第3题上的体验,总结起来其实就一句话:越像模拟题的题,越要警惕。算法的直觉重要,但真正能救命的是在草稿纸上多写两个反例的耐心。希望这篇复盘对你也有用。