news 2026/10/14 10:14:12

蓝桥杯分糖果题解:环形数组同步更新与模拟算法实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
蓝桥杯分糖果题解:环形数组同步更新与模拟算法实战

1. 从"分糖果"这道题看环形模拟的通用套路

蓝桥杯历届试题里,"分糖果"是一道非常经典的模拟题,几乎每一届备赛的学生都会在练习列表里遇到它。题目本身描述不复杂:一群小朋友围成一圈,每人手里有若干颗糖果,每一轮每个人都把自己手里糖果的一半分给右边的人,如果分完之后手里的糖果数是奇数,老师就额外补一颗给他,问经过多少轮之后,所有小朋友手里的糖果数量会变得完全一样。

这道题看起来像是小学数学应用题,但它背后考察的东西其实很扎实:环形数组的遍历、同步更新的坑、循环终止条件的判断、以及模拟类题目最常见的"状态快照"思维。很多同学第一次写这道题的时候,代码逻辑看起来没问题,跑出来结果却不对,或者干脆陷入死循环,根本原因往往不是算法不会,而是没有处理好"同一轮内数据不能互相污染"这个细节。

这篇文章我会从零开始,把这道题的完整解法拆开讲清楚。不管你是刚接触蓝桥杯的新手,还是已经刷过一些题但总在模拟题上翻车的选手,都能从里面拿到可以直接用的东西。我会先讲清楚题目到底在模拟什么过程,然后讲为什么大多数人第一版代码会写错,接着给出完整的Java实现,再深入分析几个容易踩的坑,最后聊一聊这类"环形同步更新"题目的通用解题框架,让你遇到类似的题也能快速反应过来。

关键词方面,这道题涉及的核心概念包括:环形数组、同步更新、模拟算法、奇偶判断、循环终止条件、状态快照。这些词你在读题解的时候会反复看到,我会在具体场景里把它们讲透,而不是干巴巴地丢定义。

2. 题目到底在模拟什么:把规则翻译成代码逻辑

2.1 规则逐条拆解

先把题目规则一条一条翻译成人话和代码逻辑。

第一,所有小朋友围成一个圈。这意味着数组是环形的,第i个小朋友的右边是第i+1个,而最后一个小朋友的右边是第0个。在代码里,这个"右边"用取模运算(i + 1) % n就能表达,不需要真的去构造一个环形链表。

第二,每一轮,每个人把自己手里糖果的一半分给右边的人。注意这里有个隐含前提:题目通常保证每个人初始糖果数是偶数,或者至少保证"一半"是整数。如果某个人手里是奇数,分一半就会出现小数,所以题目一般会设定初始值为偶数,或者在规则里说明奇数情况怎么处理。实际题目中,初始糖果数都是偶数,分完之后才可能出现奇数。

第三,分完之后,如果某个人手里的糖果数是奇数,老师补一颗,变成偶数。这一步的目的是保证下一轮"分一半"的时候还是整数。你可以理解为老师在帮所有小朋友维持一个"糖果数永远是偶数"的不变量。

第四,问经过多少轮,所有人的糖果数变得完全相同。

把这四条串起来,一轮的完整流程是:先算出每个人要分出去的量(当前糖果数的一半),然后每个人同时把这份量给右边的人,同时自己收到左边人给的分量,接着检查自己手里的数量是不是奇数,是的话加一。一轮结束,判断是否所有人相等,相等就输出轮数,不相等就继续下一轮。

2.2 为什么"同时"这两个字是整道题的核心

上面流程里最关键的两个字是"同时"。很多同学写代码的时候,习惯性地写成这样:

for (int i = 0; i < n; i++) { int give = candies[i] / 2; candies[i] -= give; candies[(i + 1) % n] += give; }

这段代码看起来完全符合"每个人分一半给右边"的描述,但它错得很隐蔽。问题在于:当处理第i个人的时候,candies[(i+1)%n]已经被更新过了吗?还没有,因为第i+1个人还没处理。但是当处理到第i+1个人的时候,他手里的糖果数已经被第i个人加过了,这时候他再除以2分出去,分出去的就不是"这一轮开始时他手里糖果的一半",而是"这一轮开始时的一半加上刚收到的那部分的一半"。

这就破坏了"同时"的语义。真实的场景是:所有人先同时把手里的糖果分出去,然后再同时收到别人分来的糖果。这两个动作在时间上是分开的,不能交叉进行。

所以正确的做法是:先把每个人这一轮要分出去的量算出来存到一个临时数组里,然后再统一进行加减操作。这个临时数组就是所谓的"状态快照"。

2.3 一轮操作的伪代码

把上面的分析整理成伪代码:

每一轮: 1. 对于每个 i,计算 give[i] = candies[i] / 2 2. 对于每个 i: candies[i] -= give[i] // 自己分出去的 candies[(i+1)%n] += give[i] // 右边的人收到的 3. 对于每个 i: 如果 candies[i] 是奇数,candies[i] += 1 4. 检查是否所有 candies[i] 相等 相等 -> 输出轮数,结束 不相等 -> 轮数加一,回到步骤1

这个伪代码里,步骤1和步骤2是分开的,步骤1先把所有"要分出去的量"固定下来,步骤2再统一执行加减。这样就不会出现"边分边收导致数据污染"的问题。

3. 第一版代码为什么总是跑不对:三个高频翻车点

3.1 翻车点一:边遍历边修改导致的连锁污染

上面已经提到了,这是最最常见的错误。我再举一个更具体的例子帮你理解。

假设有4个小朋友,糖果数分别是[2, 4, 6, 8]。按照错误写法:

  • 处理第0个:give = 1,candies[0] 变成 1,candies[1] 变成 5
  • 处理第1个:此时 candies[1] 是 5,give = 2,candies[1] 变成 3,candies[2] 变成 8
  • 处理第2个:此时 candies[2] 是 8,give = 4,candies[2] 变成 4,candies[3] 变成 12
  • 处理第3个:此时 candies[3] 是 12,give = 6,candies[3] 变成 6,candies[0] 变成 7

一轮下来结果是[7, 3, 4, 6]。

而正确写法应该是:

  • 先算 give 数组:[1, 2, 3, 4]
  • 统一执行:candies[0] = 2 - 1 + 4 = 5,candies[1] = 4 - 2 + 1 = 3,candies[2] = 6 - 3 + 2 = 5,candies[3] = 8 - 4 + 3 = 7
  • 检查奇数补一:[6, 4, 6, 8]

你看,两种写法结果完全不同。错误写法得到[7, 3, 4, 6],正确写法得到[6, 4, 6, 8]。如果你用错误写法去跑,可能永远也等不到所有人相等的那一天,程序就死循环了。

提示:凡是题目里出现"同时""一起""每个人"这类词,你就要警惕是不是需要先做快照再统一更新。这是模拟题的一条铁律。

3.2 翻车点二:奇数补一的时机搞错

第二个容易错的地方是补一的时机。有的同学会在分糖果之前先检查奇数补一,有的会在分完之后补,还有的会在判断相等之后补。这三种时机得到的结果是不一样的。

正确的时机是:分完糖果之后,判断相等之前。因为题目说的是"分完之后如果手里是奇数,老师补一颗",补完之后才进入下一轮的判断。如果你在分之前补,那分出去的量就变了;如果你在判断相等之后补,那这一轮的判断就用了错误的数据。

还有一个细节:补一之后,这个人的糖果数一定是偶数,这保证了下一轮分一半的时候不会出现小数。这个不变量是整道题能持续进行下去的基础。

3.3 翻车点三:循环终止条件写得太随意

第三个坑是循环终止条件。有的同学写成"只要有两个人的糖果数不相等就继续",有的写成"所有人的糖果数都等于第一个人的糖果数就结束",还有的用while(true)然后在里面判断。

这几种写法在逻辑上其实差不多,但有一个隐藏问题:如果初始状态就已经所有人相等,轮数应该是0还是1?题目一般问的是"经过多少轮之后变得相同",如果一开始就相同,那答案应该是0。但很多同学的代码会先执行一轮再判断,导致输出1。

所以正确的结构应该是:先判断是否已经全部相等,如果相等直接输出0;否则进入循环,每执行一轮轮数加一,然后再判断,相等就输出当前轮数。

另外,判断所有人相等的时候,不要用"两两比较"的写法,那样代码又长又容易错。直接用第一个人的糖果数作为基准,遍历一遍看有没有不同的就行。

4. 完整Java实现与逐行注释

4.1 完整代码

下面是我自己写的一版实现,结构清晰,注释到位,可以直接拿去提交:

import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); // 小朋友人数 int[] candies = new int[n]; for (int i = 0; i < n; i++) { candies[i] = sc.nextInt(); // 每个人初始糖果数 } sc.close(); int rounds = 0; // 记录轮数 // 如果一开始就全部相等,直接输出0 if (allEqual(candies)) { System.out.println(0); return; } while (true) { // 第一步:计算每个人要分出去的量(快照) int[] give = new int[n]; for (int i = 0; i < n; i++) { give[i] = candies[i] / 2; } // 第二步:统一执行"分出去"和"收进来" for (int i = 0; i < n; i++) { candies[i] -= give[i]; // 自己分出去的 candies[(i + 1) % n] += give[i]; // 右边的人收到的 } // 第三步:奇数补一 for (int i = 0; i < n; i++) { if (candies[i] % 2 == 1) { candies[i] += 1; } } rounds++; // 一轮结束,轮数加一 // 第四步:判断是否全部相等 if (allEqual(candies)) { System.out.println(rounds); break; } } } // 判断数组中所有元素是否相等 private static boolean allEqual(int[] arr) { for (int i = 1; i < arr.length; i++) { if (arr[i] != arr[0]) { return false; } } return true; } }

4.2 关键行逐条解释

give[i] = candies[i] / 2;这一行是快照的核心。它把当前这一轮每个人要分出去的量固定下来,后面的加减操作都基于这个快照,不会受到其他操作的影响。

candies[i] -= give[i];和candies[(i + 1) % n] += give[i];这两行是统一更新。注意(i + 1) % n这个取模操作,它让最后一个小朋友的右边回到第0个,实现了环形效果。你不需要真的去构造环形结构,一个取模就够了。

if (candies[i] % 2 == 1) { candies[i] += 1; }这一行是奇数补一。用% 2 == 1判断奇数,比用% 2 != 0更直观,因为糖果数不可能是负数。

allEqual方法用第一个元素作为基准,遍历一遍看有没有不同的。这个写法比两两比较简洁得多,时间复杂度是 O(n),完全够用。

4.3 复杂度分析

时间复杂度方面,每一轮需要遍历数组三次(算快照、统一更新、奇数补一),每次都是 O(n),所以一轮是 O(n)。轮数取决于数据,最坏情况下可能会比较多,但题目一般保证在有限轮内收敛。整体复杂度可以认为是 O(rounds * n)。

空间复杂度是 O(n),主要是candies数组和give数组。give数组其实可以优化掉,但为了代码清晰,保留它更好。

5. 实测中的边界情况与调试技巧

5.1 边界情况一:只有一个人

如果 n = 1,那这个人自己分给自己,糖果数不变,一开始就全部相等,输出0。我的代码里先判断allEqual,所以这种情况会直接输出0,不会进入循环。如果你把判断放在循环里面,可能会输出1,那就错了。

5.2 边界情况二:初始就全部相等

和上面类似,初始就相等的情况答案应该是0。这个边界很容易被忽略,因为大多数测试用例的初始状态都不是全部相等的。但蓝桥杯的测试数据里经常会有这种"刁钻"的用例,专门用来卡那些没有处理边界的代码。

5.3 边界情况三:糖果数很大

如果糖果数很大,比如接近 int 的最大值,那么candies[i] += give[i]的时候可能会溢出。不过蓝桥杯的题目一般不会给这么大的数据,用 int 就够了。如果你不放心,可以把 int 换成 long,代价很小。

5.4 调试技巧:打印每一轮的状态

如果你跑出来的结果不对,最有效的调试方法是在每一轮结束后打印当前所有糖果数。这样你可以一眼看出是哪一轮开始出问题的,然后倒推是哪个步骤写错了。

// 调试用:打印每一轮的状态 System.out.println("第 " + rounds + " 轮: " + java.util.Arrays.toString(candies));

把这行代码加在rounds++之后,跑几个小用例,你就能清楚地看到每一轮的变化过程。这个方法我用了很多年,几乎能解决90%的模拟题调试问题。

5.5 一个容易忽略的细节:补一之后要不要重新判断

有的同学会想:补一之后,这个人的糖果数变了,会不会影响"所有人相等"的判断?答案是会,但不需要额外处理。因为补一是这一轮的一部分,补完之后才进入判断,判断用的就是补完之后的最终状态。你不需要在补一之后单独再判断一次,那样反而会多算一轮。

6. 从这道题延伸出的环形模拟通用框架

6.1 环形数组的三种处理方式

处理环形数组,常见的有三种方式:

第一种是取模法,也就是(i + 1) % n。这是最简洁的方式,不需要额外空间,代码也最直观。缺点是每次访问都要算一次取模,性能上有一点点开销,但对于这种题目完全可以忽略。

第二种是复制数组法,把原数组复制一份接到后面,变成[a, b, c, a, b, c],然后按普通数组处理。这种方式适合需要频繁访问"右边若干个"的场景,但会浪费一倍空间。

第三种是哨兵法,在数组末尾加一个特殊值,处理到末尾的时候特殊处理。这种方式代码最不优雅,一般不推荐。

对于"分糖果"这道题,取模法是最合适的。你只需要记住(i + 1) % n这个写法,就能应付绝大多数环形遍历的场景。

6.2 同步更新的通用模板

同步更新的通用模板就是"快照 + 统一更新":

1. 创建快照数组,把当前状态复制一份 2. 基于快照计算每个位置的新值 3. 把新值写回原数组

这个模板适用于所有"每个位置的新值依赖于其他位置的旧值"的场景。比如生命游戏、细胞自动机、环形分糖果,都是这个套路。你只要记住这个模板,遇到类似的题就能快速反应过来。

6.3 模拟题的通用解题步骤

最后总结一下模拟题的通用解题步骤,这是我刷了几百道模拟题之后总结出来的:

第一步,把题目规则逐条翻译成代码逻辑,不要跳步,不要想当然。

第二步,找出所有"同时"发生的操作,这些操作大概率需要快照。

第三步,确定循环的终止条件,以及初始状态是否已经满足终止条件。

第四步,处理边界情况,比如 n=1、初始就满足条件、数据溢出等。

第五步,写完之后用小数据手动模拟一遍,看看结果对不对。

第六步,如果不对,打印中间状态,定位问题。

这套步骤看起来简单,但真正能每次都严格执行的人不多。我自己在比赛的时候,因为跳过第一步直接写代码,翻车过好几次。后来强迫自己每次都先把规则写清楚,正确率才稳定下来。

6.4 这道题还能怎么变

"分糖果"这道题其实有很多变种。比如把"分一半给右边"改成"分一半给左边",或者改成"分三分之一给右边、三分之一给左边",或者改成"每个人分给右边的人的数量等于自己糖果数除以3的余数"。这些变种的核心逻辑是一样的,都是环形同步更新,你只要掌握了快照 + 统一更新的套路,改起来就是几分钟的事。

还有一种变种是问"经过多少轮之后,所有人的糖果数之和最小",这种就需要在模拟的过程中记录最小值,稍微复杂一点,但本质还是模拟。

我在实际练习的时候,会把同一道题的多个变种放在一起做,这样能更快地识别出哪些是核心逻辑,哪些是表面变化。这个方法对提升模拟题的解题速度非常有效。

7. 我在刷这道题时踩过的真实坑

第一次遇到"分糖果"这道题的时候,我写的代码就是最上面说的那种错误写法,边遍历边修改。跑样例的时候,样例太简单,刚好没暴露出问题,我还以为自己做对了。结果提交上去,一半的测试用例超时,另一半答案错误。

后来我把每一轮的状态打印出来,才发现数据从第一轮开始就偏了。当时我盯着那段代码看了很久,一直没想明白哪里错了,因为逻辑上"每个人分一半给右边"就是这么写的啊。直到我把"同时"这两个字圈出来,才恍然大悟。

这个坑给我的教训是:模拟题里,题目的每一个字都是有用的,尤其是那些看起来像是废话的修饰词。"同时""一起""每个人"这些词,往往就是区分正确解法和错误解法的关键。

后来我又踩了一个坑,是补一的时机。我一开始把补一放在了分糖果之前,因为我想着"先保证是偶数再分",结果答案也是错的。正确的顺序是"先分,分完再看是不是奇数,是的话补一"。这个顺序不能乱。

还有一个坑是初始就相等的情况。我一开始的代码是先执行一轮再判断,导致初始就相等的用例输出1而不是0。这个坑很小,但很致命,因为蓝桥杯的测试数据里一定有这种用例。

这三个坑我前后花了大概两个小时才全部填上。现在回头看,其实都是很基础的问题,但在当时就是想不到。所以我把它们写出来,希望你能少走点弯路。

注意:模拟题的调试,最有效的方法永远是打印中间状态。不要靠脑子想,脑子会骗你,打印出来的数据不会。

8. 给不同基础读者的练习建议

如果你是完全的新手,刚学完Java基础语法,我建议你先不要急着写这道题。先把数组的遍历、取模运算、方法的定义和调用这几个基础点练熟。然后找一道更简单的模拟题,比如"求数组中最大值"或者"数组元素循环右移",练练手。等这些都没问题了,再回来做"分糖果"。

如果你已经有一些基础,但模拟题总是写不对,我建议你专门花一个下午的时间,把这道题反复写三遍。第一遍看着题解写,第二遍不看题解写,第三遍换一种数据结构写(比如用队列模拟环形)。三遍下来,你对环形同步更新的理解会深刻很多。

如果你已经比较熟练了,那你可以试试这道题的变种,或者去找一些更复杂的模拟题,比如"约瑟夫环"或者"生命游戏"。这些题的核心套路是一样的,但细节更复杂,能帮你把模拟题的解题能力再往上提一个台阶。

最后说一句,模拟题在蓝桥杯里占的比重不小,而且这类题的特点是"看起来简单,写起来容易错"。很多人觉得模拟题不需要算法,就不重视,结果比赛的时候在这上面丢分。我的建议是,把模拟题当成算法题来练,每一道都认真对待,把边界情况和调试方法都练熟。这样到了赛场上,你才能稳稳地把该拿的分拿到手。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 10:14:08

大模型分布式优化实战:从通信、显存到计算的全栈调优

1. 项目概述&#xff1a;为什么“大模型分布式优化”不是锦上添花&#xff0c;而是生死线你刚跑通一个7B参数的模型&#xff0c;在单卡A100上训了三天&#xff0c;loss曲线终于开始平缓——结果一测推理延迟&#xff0c;P99高达2.8秒&#xff0c;线上服务直接超时熔断。这不是个…

作者头像 李华
网站建设 2026/10/14 10:13:17

威胁情报驱动的恶意软件检测系统设计与实践

简介&#xff1a;本资源是一个轻量级的基于威胁情报的恶意软件检测系统实现&#xff0c;面向网络安全初学者、高校安全课程实践者及渗透测试入门人员&#xff0c;聚焦于将威胁情报与基础检测逻辑落地为可运行代码&#xff0c;解决传统静态扫描难以应对新型/变种恶意软件的问题。…

作者头像 李华
网站建设 2026/10/14 10:13:03

Go语言隐式接口设计解析与工程实践

1. 从显式到隐式&#xff1a;Go语言接口设计的矛盾与统一第一次看到Go语言的接口设计时&#xff0c;我正坐在咖啡厅里调试一个Python的鸭子类型问题。当了解到Go这种静态类型语言居然采用了隐式接口机制&#xff0c;我的第一反应是——这简直是对传统OOP的"背叛"&…

作者头像 李华
网站建设 2026/10/14 10:12:40

大整数相加与十六进制算法实现解析

1. 大整数相加算法解析与实现 1.1 问题背景与需求分析 大整数相加是计算机科学中一个经典的基础算法问题。在标准编程语言中&#xff0c;整型变量通常有固定的存储空间限制&#xff08;如32位或64位&#xff09;&#xff0c;无法直接处理超过其表示范围的大整数。例如&#xf…

作者头像 李华
网站建设 2026/10/14 10:12:31

道路天气鲁棒性数据集:17000张多气象标注图像

简介&#xff1a;本资源是一个面向计算机视觉初学者与进阶研究者的道路场景图像分类数据集&#xff0c;聚焦真实天气变化对道路识别的影响&#xff0c;适用于自动驾驶感知模块训练、鲁棒性分类模型验证及多条件泛化能力研究。数据集共约17,000张标注图像&#xff0c;已按晴天、…

作者头像 李华
网站建设 2026/10/14 10:12:29

STM32F407 USB虚拟串口CDC开发全指南:从枚举失败到稳定通信

简介&#xff1a;本资源是基于STM32F407微控制器实现USB虚拟串口&#xff08;VCP&#xff09;功能的完整嵌入式开发工程&#xff0c;面向嵌入式初学者、单片机开发者及STM32进阶学习者&#xff0c;解决USB CDC类设备从零搭建、枚举识别、主机通信与固件调试等核心问题。压缩包含…

作者头像 李华