华为OD机试的题库年年换新,但有些题型的套路非常稳定。2025年C卷里这道"魔法积木消消乐",用Java和Go都能解,网上能搜到的讨论也不少,但相当一部分题解给的思路是"单调栈扫一遍",看着轻巧,实际一跑复杂样例就翻车。我这次把题目还原、把错误思路为什么错讲透、把Java和Go两版完整实现都写出来,再附上我实测过的多组用例,给正在备战的读者一份能直接照着用的参考答案。
先说清楚,真题的原始题面我记不全,下面这份描述是结合多个渠道还原出来的版本,华为OD机试不同批次在细节上可能略有出入,但核心规则差异不大。另外一点必须提前讲:双机位C卷的考试环境和自己平时在IDE里刷题完全是两回事,代码怎么组织、时间怎么分配、输入输出怎么写,这些细节我会放到最后专门说,别小看这些,很多人的编译分就丢在这上面。
1. 题目还原与核心规则拆解
1.1 题面与输入输出约定
题目大概是这样的:有一排魔法积木,每块积木上印着一个数字(0到9),表示积木的颜色。输入是一串数字字符,长度N在1到1000之间。游戏规则是:如果存在连续且颜色相同、长度不少于3的积木段,这一段就会被整体消除。消除后,左右两边的积木立刻靠拢拼接;拼接后如果又出现新的连续相同且长度不少于3的积木段,就会继续自动消除,直到整个序列稳定,不再有任何长度大于等于3的连续同色段为止。要求输出最终剩余的积木数量,如果全部消除,输出0。
输入约定是一行字符串,比如"1122233344"。输出是一个整数。题目本身不复杂,但规则里埋着两个容易踩的坑,不先想清楚,后面算法怎么写都是错的。
1.2 两个容易忽略的规则细节
第一个细节是"长度不少于3"到底是消3个还是整段消。拿"1111"举例:按"消除恰好3个"的规则,消掉3个,剩下1个;按"整段消除"的规则,因为4个1连续相同且长度4>=3,整段4个一起消失,最终剩余0。这两种解释在真实机试里都可能出现,取决于题面具体怎么措辞。如果题面写的是"消除任意三个连续相同积木",那思路完全是另一套;如果写的是"连续相同不少于3的积木段自动消除",那就是本文讲的整段消除。我建议拿到题先花30秒确认这个规则,再决定用哪种模型。
第二个细节是消除后的连锁反应。很多第一次做这题的人会以为只要扫一遍找出所有长度>=3的段删掉就完事了,但"112221122211"这种数据会立刻教做人。中间一截被删掉后,左右两边的同色积木会跨过原来被删的位置拼到一起,拼出来的新段长度可能又够了,又触发下一轮消除。这个连锁过程不是一次扫描能搞定的。
1.3 示例推演
看官方风格的示例:
输入:"1222333444"
过程是这样的:先看整串,222是连续3个2,达到消除条件,消除后得到1333444;接着333连续3个,消除后得到1444;接着444连续3个,消除后得到1。最终剩余1块积木,输出1。
再看一个带连锁的例子:
输入:"1122233344"
先消222,得到1133344;再消333,得到1144。此时44只有2个,不够3个,序列稳定,输出4。
注意这个例子中,消完222后剩余"11"和"33344"拼成了"1133344",里面333依然达到条件,所以连锁触发。这种"消一个带出下一个"的场景是本题的核心考察点,后面我给出的多轮扫描方案就是围绕这个设计的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么网上流传的"单调栈"解法会算错
2.1 常见解法长什么样
这题在网上最流行的写法是单调栈:维护一个栈,栈里存(颜色, 已连续数量)。遍历每个积木,如果栈顶颜色和当前积木相同,就给栈顶计数加1;否则压入一个新的(当前颜色, 1)。每次更新完栈顶,检查栈顶计数是否达到3,达到就弹出。最后栈里剩余元素的数量和就是答案。
这个思路看起来天衣无缝,时间复杂度O(n),代码也短,很多博客直接拿来当标准答案。但它有一个致命的隐含假设:当前积木一旦入栈,它的状态就被固定了,后面出现的同色积木只能通过栈顶来合并。可实际上,当中间某些积木被消除后,物理上不相邻的积木段会拼接成新段,新段的长度可能跨越多个"曾经被消除"的位置,而单调栈已经把这些位置的信息丢了。
2.2 一个反例让错误暴露
用字符来说明更直观,假设输入是"aabbbaabbbaa"(数字版就是"112221122211",a对应1,b对应2)。
正确答案是多少?我手推一遍:原串是 aa bbb aa bbb aa。
第一轮找到第一个可消段bbb(中间那一组),消掉后,左边aa和右边aa拼成aaaa,序列变成 aaaa bbb aa。此时aaaa长度4>=3,整段消除,序列变成 bbb aa。再消bbb,最后剩下aa,也就是2个a,输出2。
等等,不对,我重新检查。原串 "aabbbaabbbaa" 展开是:a a b b b a a b b b a a。先消第一组bbb(第3到第5位),剩余 a a a a b b b a a。此时前4位是aaaa,消除,剩余 b b b a a。再消bbb,剩余 a a,输出2。所以正确答案是2。
如果按照每次消除所有可消段("同时消"规则),第一轮段划分是:aa(2)、bbb(3)、aa(2)、bbb(3)、aa(2),消掉两个bbb后剩余 aaaaaa(6个a),再消掉,剩余0。这就产生了矛盾!两种消除顺序得出不同结果?
这里要命了,我得把这个规则完全定义清楚。让我重新审题:题面说"每次消除一段连续且颜色相同、长度不少于3的积木",这是操作型规则,玩家可以自己选择消除哪一段。而机试通常考的是自动消除规则:只要存在这样的段就自动消,消到不能消为止。如果"玩家可操作",那么不同选择顺序是否影响最终剩余数量?这个题目必须保证结果唯一,否则没法判题。
实际上,华为OD这道题的原始规则大概率是"自动消除",而且消除顺序不影响最终结果(这类问题具有合流性)。但上面的反例说明"先消哪一组bbb"可能导致不同结果?我再仔细手推一次。
原串:a a b b b a a b b b a a
如果先消第一组bbb(位置3-5):剩余 a a(位置1-2)+ a a(位置6-7)+ b b b(位置8-10)+ a a(位置11-12)= a a a a b b b a a。消除aaaa -> b b b a a -> 消除bbb -> a a,结果2。
如果先消第二组bbb(位置8-10):剩余 a a + b b b + a a + a a = a a b b b a a a a。消除中间bbb -> a a a a a a(6个a) -> 消除6个a -> 0。结果0!
确实不同!这就说明"消除顺序不同导致结果不同"的版本是存在的。但机试题必须给出确定答案,所以要么题面明确规定了消除顺序(比如"每次消除最靠左的可消段"),要么规定"所有可消段同时消除并循环",要么题目本身限定这种情况不会出现。
好,为了博文严谨,我重新定义题目规则为**"所有长度>=3的连续同色段同时消除,然后拼接,重复直到稳定"**。这个规则是确定的,无歧义,判题也稳定。用这个规则,"aabbbaabbbaa" 第一轮消掉两个bbb -> 拼接后 aaaaaa(6个a)-> 第二轮全部消除 -> 0。最终输出0。
那么这个反例还能不能打脸"每插入一个字符就弹栈"的单调栈解法?用它跑一下:读a,a -> (a,2);读b,b,b -> 栈顶变成(b,3)弹出,栈剩(a,2);读a,a -> 第一个a和栈顶(a,2)合并成(a,3)弹出,第二个a入栈(a,1);读b,b,b -> (a,1),(b,3)弹出,栈剩(a,1);读a,a -> 压成(a,3)弹出,栈空。结果是0。
啊,这个例子用单调栈跑出来的结果是0,恰好和"同时消"规则一致。但它还是错的,因为它是碰巧。我需要找一个:在"同时消"规则下正确结果非空或为空,但单调栈给出相反答案的例子。
还是回到之前我发现的例子:"aabbbaabbbaa"如果按"同时消"答案是0,单调栈也是0,一致。那单调栈到底什么时候错?
我之前发现的错误场景是"aabbbaa bbb aa"这种,即 aabbbaabbbaa 按"顺序消"是2,但"同时消"是0,而单调栈给出0。这说明单调栈更符合"同时消"规则?不一定,再看一个例子:
构造:aa bbb aa bbb aa bbb aa,即 "aabbbaabbbaabbbaa"(a=1,b=2,数字版"11222112221122211")。
按"同时消"规则:第一轮段划分 aa(2), bbb(3), aa(2), bbb(3), aa(2), bbb(3), aa(2),消除所有bbb,剩余 aaaaaaaaaa(10个a),第二轮消除全部 -> 0。
用单调栈(插入一个字符就合并+弹栈):
- a,a -> (a,2)
- b,b,b -> (a,2),(b,3)弹出 -> (a,2)
- a,a -> 第一个a合并(a,3)弹出,第二个a入栈(a,1)
- b,b,b -> (a,1),(b,3)弹出 -> (a,1)
- a,a -> 第一个a合并(a,2),第二个a合并(a,3)弹出,栈空
- b,b,b -> (b,3)弹出,栈空
- a,a -> (a,2)
结果2?不对,这又是错的。
正确答案是0,单调栈给出2。这就是一个优秀反例。我上一轮的手算可能有误,再仔细推:
原串:a a b b b a a b b b a a b b b a a
单调栈模拟:
- a -> (a,1)
- a -> (a,2)
- b -> (a,2),(b,1)
- b -> (a,2),(b,2)
- b -> (a,2),(b,3) -> 弹出b,栈(a,2)
- a -> 与(a,2)合并 -> (a,3) -> 弹出,栈空
- a -> 入栈(a,1)
- b -> (a,1),(b,1)
- b -> (a,1),(b,2)
- b -> (a,1),(b,3) -> 弹出,栈(a,1)
- a -> (a,2)
- a -> (a,3) -> 弹出,栈空
- b -> (b,1)
- b -> (b,2)
- b -> (b,3) -> 弹出,栈空
- a -> (a,1)
- a -> (a,2)
最终剩(a,2),即2个a。错误,正确答案0。
这个反例更清晰。我在博文里采用这个来展示单调栈的错误
