LeetCode 1054:距离相等的条形码 (贪心算法)—— 题解

👋 欢迎阅读

🎯 欢迎来到「距离相等的条形码」题解之旅! 本文将带你从“重新排列条形码使相邻元素不同”这一排列约束问题出发,深入理解贪心 + 频率优先放置的经典策略,并掌握如何通过先放最多的元素,再隔空填充其余元素来高效构造合法排列。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 1054 题,给定一个条形码数组,要求重新排列使得任意两个相邻的条形码不能相同,题目保证有解。本质上,这是一个频率调度问题——出现次数最多的元素是瓶颈,只要把它每隔一个位置放一个,其余元素填满剩余空位,就能保证不相邻。

  • 明确学习目标:掌握贪心构造法——先用哈希表统计每个数字的出现频率,并找出频率最高的元素;然后优先将该元素放在偶数位(0, 2, 4, ...),以最大程度拉开距离;接着将剩余元素按任意顺序填充到剩下的偶数位和奇数位(偶数位填满后填奇数位)。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 barcodes = [1,1,1,2,2,2] 输出 [2,1,2,1,2,1] 等合法结果)。

本文将从问题转化、贪心构造策略(频率统计 + 隔位放置)、代码模拟到复杂度分析,层层递进。即使你对贪心构造还不熟悉,我们也会从“先把最多的隔开放,剩下的随便塞”这一直觉出发,让你轻松抓住核心思想——只要最多的那个不挨着自己,其他元素就更容易被隔开,整体排列就一定合法。现在,让我们一起重新排列条形码,让相同的元素都“保持距离”吧! 📦🔀


一、题目

1054. 距离相等的条形码 - 力扣(LeetCode)

二、做题思路

一、问题分析(前置分析)

给定一个条形码数组,要求重新排列,使得任意两个相邻的条形码不相等。题目保证存在答案。
核心观察:这个问题等价于 “间隔排列” —— 将出现次数最多的元素优先插入到偶数位(0, 2, 4, ...),能最大限度地将其隔开,避免相邻。

鸽巢原理保证有解:在长度为 n 的数组中,若某种元素出现次数为 maxCount,则要使其不相邻,必须满足 maxCount ≤ (n + 1) / 2。因为最极端的情况是将该元素放在所有偶数位(共 (n+1)/2 个),若超过这个数,则必然会有两个该元素相邻。题目保证有解,即该条件成立。


二、贪心策略(核心决策规则)

  • 第一步:使用哈希表统计每个数字出现的频率,并找出 出现次数最多的数字 maxVal 及其次数 maxCount

  • 第二步:将 maxVal 优先放置在结果数组的 偶数位(下标 0, 2, 4, ...),这样能最大程度地将其分隔开。

  • 第三步:从哈希表中移除 maxVal,然后将其余数字 任意顺序 填充到剩余的空位中(继续按偶数位填充,若偶数位已满则转向奇数位)。

  • 第四步:返回重排后的数组。


三、正确性说明(简单版本)

鸽巢原理指出:要避免相同元素相邻,出现次数最多的元素必须不超过 (n+1)/2。题目保证有解,因此该条件天然成立。

贪心策略的核心是 “先放最多的”:将出现次数最多的数字放在所有偶数位,能保证它们之间至少隔一个其他数字(因为偶数位之间至少相差 2)。如果偶数位不够放,则说明最大次数 ≤ (n+1)/2,偶数位足够容纳所有该数字。
其余数字无论放在剩余的任何位置,都不会与 maxVal 相邻(因为 maxVal 只占偶数位,剩余位置均为奇数位,相邻位置中不会出现两个 maxVal)。因此这种放置方式必然能构造出合法排列,且算法一定能得到正确答案。


四、实现细节(边界防护)

  • 使用 unordered_map<int, int> 统计频率,同时记录 maxValmaxCount

  • 结果数组 ret 初始化为 n 个 0。

  • 放置 maxVal:用 index = 0 开始,每次 index += 2,循环 maxCount 次。

  • 移除 maxVal 后,遍历剩余元素,对于每个元素 x 和其次数 y,循环 y 次:

    • index >= n,则切换到奇数位:index = 1

    • x 放在 ret[index],然后 index += 2

  • 最后返回 ret

  • 时间复杂度 O(n),空间复杂度 O(n)。


五、返回值(目标映射)

返回重排后的数组 ret,即 满足相邻元素不相等的一种排列

四、代码

class Solution 
{
public:
    vector<int> rearrangeBarcodes(vector<int>& barcodes) 
    {
        // 1. 统计每个数字出现的频率,并找出出现次数最多的数字(maxVal)及其次数(maxCount)
        //    贪心策略:将出现次数最多的数字优先放置,且每隔一个位置放一个,避免相邻。
        unordered_map<int, int> hash;
        int maxVal = 0, maxCount = 0;
        for (auto x : barcodes) 
        {
            hash[x]++;
            if (maxCount < hash[x]) 
            {
                maxVal = x;
                maxCount = hash[x];
            }
        }
    int n = barcodes.size();
    vector&lt;int&gt; ret(n);   // 结果数组

    // 2. 将出现次数最多的数字放在偶数位(0, 2, 4, ...)
    //    这样能最大程度地将它们分隔开,避免相邻。
    int index = 0;
    for (int i = 0; i &lt; maxCount; i++) 
    {
        ret[index] = maxVal;
        index += 2;   // 每次跳到下一个偶数位
    }

    // 3. 从哈希表中移除已放置最多的数字,然后将其余数字按任意顺序填充到剩余位置
    hash.erase(maxVal);

    // 遍历剩余的数字及其次数
    for (auto&amp; [x, y] : hash)   // 结构化绑定(C++17),需要编译支持
    {
        for (int i = 0; i &lt; y; i++) 
        {
            // 如果当前偶数位已经填满(index &gt;= n),则切换到奇数位(从1开始)
            if (index &gt;= n) 
            {
                index = 1;
            }
            ret[index] = x;
            index += 2;   // 继续跳到下一个偶数位(或奇数位)
        }
    }

    // 4. 返回重排后的数组(保证相同数字不相邻)
    return ret;
}
};

五、流程图

六、正确性说明(详细版)

步骤 1:符号与问题建模
+----------------------------------------------------+
|  输入:条形码数组 barcodes,长度 n                 |
|  目标:重新排列,使任意相邻两个元素不相同          |
|  保证存在解                                         |
+----------------------------------------------------+
                    |
                    v
+----------------------------------------------------+
|  贪心策略:                                         |
|  ① 统计每个数字出现频率,找到出现次数最多的数 x, |
|     记其次数为 maxCount。                          |
|  ② 将 x 优先放置在 **偶数位置**(0, 2, 4, ...),  |
|     每隔一个位置放一个,以最大程度隔开它们。        |
|  ③ 其余数字任意填充剩余位置(继续隔位填充)。       |
|  贪心实质:**出现次数最多的数是最难处理的**,       |
|  优先安排它们并间隔放置,能保证后续填充不冲突。     |
|  鸽巢原理保证:因为 maxCount ≤ (n+1)/2,           |
|  所以偶数位(共 (n+1)/2 个)足够容纳所有 x。       |
+----------------------------------------------------+
  • 设数组长度为 n,某个数字的出现次数为 cnt

  • 鸽巢原理:若某数字出现次数超过 (n+1)/2,则无论怎样排列,都会有两个相邻(因为 n 个位置最多能容纳 (n+1)/2 个不相邻的相同元素)。题目保证有解,因此所有数字的出现次数都不超过 (n+1)/2,尤其是最大次数 maxCount ≤ (n+1)/2

  • 贪心核心:将出现次数最多的数字先放在偶数索引上,因为偶数位数量恰好是 (n+1)/2 个(即 ceil(n/2)),足以容纳它们,且相邻位置(i 和 i+1)不可能同时是偶数,从而避免了相邻。


步骤 2:关键性质 —— 鸽巢原理与贪心优先性的正确性(反证法)
+----------------------------------------------------+
|  性质 1(鸽巢原理可行性):若 maxCount > (n+1)/2, |
|  则不可能存在解。反证:若有超过一半的元素相同,   |
|  根据抽屉原理,n 个位置中最多只能容纳 (n+1)/2     |
|  个互不相邻的位置(即所有偶数位),超过这个数量   |
|  必然有至少两个相同元素相邻,矛盾。                |
|  因此题目保证有解 ⇒ maxCount ≤ (n+1)/2。           |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  性质 2(优先放置最多的数):若存在一个解,则      |
|  出现次数最多的数 x 必然可以(且应当)优先放置。   |
|  证明:若 x 不优先放置,则其可能被挤压到连续位置, |
|  但 x 的数量最多,最“危险”,因此优先分配它们到    |
|  偶数位是安全的,且不会影响其他数字的放置。         |
|  反证:若优先放置 x 导致无法完成,则任何其他顺序   |
|  也不可能完成(因为 x 的数量没有减少),故优先     |
|  放置 x 是最优的。                                 |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  性质 3(间隔放置的有效性):将 x 放在偶数位后,   |
|  剩余位置(奇数和未使用的偶数)形成若干连续段,     |
|  每个剩余数字的出现次数 ≤ 段长的一半(由鸽巢原理   |
|  的递推保证),因此递归填充仍可成功。               |
|  证明:每次填充后,剩余问题与原问题同构,           |
|  贪心策略可继续应用。                               |
+----------------------------------------------------+

详细论证

  • 性质1(鸽巢原理):在一个长度为 n 的数组中,要避免相邻相同,相同元素最多可以出现在 ceil(n/2) = (n+1)//2 个位置(例如所有偶数索引)。若 maxCount > (n+1)//2,则无论放在哪些位置,必然有两个落在相邻位置(因为互不相邻的位置数量不够)。题目保证有解,因此 maxCount ≤ (n+1)//2。图片中的 (n+1)/2 正是这个上界。

  • 性质2(优先放置最多的数):设 x 是出现次数最多的数字。因为 x 的数量最多,它是“最难”处理的元素。若最优解存在,则把 x 放在偶数位一定不会导致失败。用反证法:若把 x 放在偶数位后,剩余数字无法完成排列,则说明剩余空间中某个数字的出现次数超过了剩余位置的一半。但这是不可能的,因为原始数组满足鸽巢原理,且 x 占用了最分散的位置,剩余空间只会更宽松,因此原解也不存在,矛盾。所以优先放置 x 是安全的。

  • 性质3(递归可行性):放置完 x 后,剩余位置总数为 n - maxCount,其中偶数位可能还有剩余(如果 maxCount 小于偶数位总数),奇数位全部空余。剩余每个数字的出现次数仍不超过对应剩余位置数的一半(由鸽巢原理的全局约束可推出),因此问题递归地满足同一性质。贪心可以继续选择下一个出现最多的数字,重复间隔填充。


步骤 3:归纳证明 —— 间隔填充过程得到全局有效排列
+----------------------------------------------------+
|  初始:数组为空,所有元素待填充。                  |
|  归纳假设:假设前 t 次放置(每次放置一个数字的     |
|  所有副本到当前剩余的隔位位置)后,已填充部分满足  |
|  不相邻约束,且剩余未填充数字的次数 ≤ 剩余位置数   |
|  的一半(即鸽巢条件仍满足)。                       |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  第 t+1 次:选择剩余数字中出现次数最多的 y,        |
|  从当前可用位置中**每隔一个位置放置一个**(即      |
|  从上一个填充位置后跳一个),共放置 cnt[y] 个。    |
|  由性质2和性质3,该操作不会导致相邻冲突,           |
|  且剩余数字仍满足鸽巢条件,归纳假设保持。           |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  当所有数字放置完毕,数组满足任意相邻不相等。       |
|  因为每一步都保持鸽巢条件,且贪心选择保证了        |
|  冲突不会发生,所以最终排列有效。                   |
+----------------------------------------------------+

详细论证

  • 归纳基础:初始没有放置任何数字,已填充部分为空(无相邻冲突)。剩余数字的计数满足全局的鸽巢条件:maxCount ≤ (n+1)//2。由性质1,这是保证有解的必要条件,因此归纳基础成立。

  • 归纳步骤:假设已放置了部分数字,已填充位置互不相邻(每个数字都隔开),且剩余数字仍满足鸽巢条件(即剩余任何数字的出现次数 ≤ 剩余位置数的一半)。现在选择剩余数字中出现次数最多的 y(记次数为 c),将其放置在剩余位置中从当前指针开始,每跳一个位置放一个(如代码中的 index += 2)。因为 c ≤ (剩余位置数 + 1)//2(由鸽巢条件),所以这些位置一定够用,且不会相邻(因为放置间隔为2)。放置后,y 被完全消耗,剩余数字的总数减少 c,剩余位置数减少 c,且剩余数字仍满足鸽巢条件(因为原条件保证了最坏情况)。因此归纳假设保持。

  • 终止:当所有数字都被放置,每个数字在放置时都遵循间隔规则,且放置后没有出现相邻相同的情况(因为放置时总是跳过已占用位置,不会紧挨着放)。因此最终排列满足要求。由于每次选择都是当前最“危险”的数字优先放置,且每次放置都保证了不相邻,算法必然成功。

🎯 闭幕

动态规划学习路径图

🎉 恭喜你完成了「距离相等的条形码」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题采用 贪心 + 频次统计 策略:先找出出现次数最多的数字,将其 隔一个位置(即放在偶数下标)放置,然后再放置其他数字。为什么这样能保证任意相邻两个条形码不相等? 如果出现次数最多的数字数量超过总数的一半,还会成立吗?(提示:题目保证有解,所以不会超过一半)

  • 代码中先占满偶数位(0, 2, 4, ...),如果偶数位不够,再转到奇数位(1, 3, 5, ...)。为什么先放偶数位,而不是奇数位? 如果数组长度为奇数,偶数位比奇数位多一个,这种选择有什么好处?

  • 填充剩余数字时,只需按任意顺序放入剩余的空位(继续隔一个位置)。如果剩余数字中也有多个相同的,会不会出现相邻相等的情况? 为什么不会?

📚 延伸挑战

  • 如果数组中存在 三种以上 且频率都较高,间隔放置仍然有效吗?试着用 [1,1,2,2,3,3] 验证代码是否正确输出一个合法排列。

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值