【贪心算法】专题(五):逆向思维与区间重叠的极致拉扯

文章目录

    • 在数学与几何的迷宫中,洞悉最优路径
    • [一、 前言:贪心的高阶形态](#一、 前言:贪心的高阶形态)
    • [二、 坏了的计算器:逆向思维的降维打击 (Medium)](#二、 坏了的计算器:逆向思维的降维打击 (Medium))
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 贪心策略与数学证明](#2.2 贪心策略与数学证明)
      • [2.3 C++ 代码实战](#2.3 C++ 代码实战)
    • [三、 合并区间:贪心求并集 (Medium)](#三、 合并区间:贪心求并集 (Medium))
      • [3.1 题目描述](#3.1 题目描述)
      • [3.2 贪心策略与几何证明](#3.2 贪心策略与几何证明)
      • [3.3 C++ 代码实战](#3.3 C++ 代码实战)
    • [四、 无重叠区间:保留生存空间 (Medium)](#四、 无重叠区间:保留生存空间 (Medium))
      • [4.1 题目描述](#4.1 题目描述)
      • [4.2 贪心策略与留白证明(面试极易错)](#4.2 贪心策略与留白证明(面试极易错))
      • [4.3 C++ 代码实战](#4.3 C++ 代码实战)
    • [五、 用最少数量的箭引爆气球 (Medium)](#五、 用最少数量的箭引爆气球 (Medium))
      • [5.1 题目描述](#5.1 题目描述)
      • [5.2 贪心策略与交集下界证明](#5.2 贪心策略与交集下界证明)
      • [5.3 C++ 代码实战](#5.3 C++ 代码实战)
    • [六、 整数替换:二进制掩码的最优消除 (Medium)](#六、 整数替换:二进制掩码的最优消除 (Medium))
      • [6.1 题目描述](#6.1 题目描述)
      • [6.2 贪心策略与二进制证明](#6.2 贪心策略与二进制证明)
      • [6.3 C++ 代码实战](#6.3 C++ 代码实战)
    • [七、 总结](#七、 总结)

在数学与几何的迷宫中,洞悉最优路径

一、 前言:贪心的高阶形态

💬 开篇 :经过前面几篇的淬炼,我们已经掌握了单调区间、极值分类以及双指针贪心。今天,我们将步入贪心算法最经典、也是面试最常考的两大领域:逆向推导区间重叠

🚀 核心破局点

  1. 正难则反 :当正向操作的抉择树呈现指数级爆炸时,逆向操作往往具有数学上的唯一确定性
  2. 区间的吞噬与避让:面对一堆错综复杂的区间,排序是第一把利器;而决定是去"吞噬(求并集)"还是"避让(求交集最小右端点)",则需要严格的贪心逻辑。

💡 学习指南

本篇精选了 5 道高频题目。在"无重叠区间"和"合并区间"中,我将重点为你辨析为什么一个是求最大右端点,另一个是求最小右端点。摒弃死记硬背,让我们用数学证明来主宰代码!


二、 坏了的计算器:逆向思维的降维打击 (Medium)

2.1 题目描述

题目链接991. 坏了的计算器

描述

初始数字为 startValue,目标为 target。你只能执行两种操作:

  1. 乘 2(双倍)
  2. 减 1(递减)
    求将其变为 target 的最少操作次数。

2.2 贪心策略与数学证明

贪心策略(正难则反)

正向思维(start -> target)时,我们在任何一个奇数或偶数节点,都可以选择 *2-1,这棵状态树是无限发散的。
逆向思维(target -> start 时,操作变成了 /2+1。此时策略具备了唯一确定性:

  1. 如果当前 target 是奇数,只能 执行 +1 操作使其变偶数。
  2. 如果当前 target 是偶数,必定 执行 /2 操作。
  3. target <= start 时,只能无脑 +1,操作次数直接加上差值。

严格数学证明

假设当前目标为偶数 T T T。我们在逆向时,究竟是选择直接 /2,还是选择 +1, +1, /2

  • 方案A(先除) :操作为 /2+1,结果变为 T 2 + 1 \frac{T}{2} + 1 2T+1,耗费 2 步。
  • 方案B(先加) :操作为 +1+1,然后 /2,结果变为 T + 2 2 = T 2 + 1 \frac{T+2}{2} = \frac{T}{2} + 1 2T+2=2T+1,耗费 3 步。
    结论:在达到相同数值结果的前提下,方案A(遇到偶数立即除以 2)永远比方案B少走 1 步!因此,只要是偶数,毫不犹豫地除以 2 是绝对的全局最优解。

2.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int brokenCalc(int startValue, int target) {
        int ret = 0;
        
        // 逆向推导:从 target 回退到 startValue
        while (target > startValue) {
            if (target % 2 == 0) {
                // 如果是偶数,最优策略绝对是除以 2
                target /= 2;
            } else {
                // 如果是奇数,只能加 1 变成偶数
                target += 1;
            }
            ret++;
        }
        
        // 当 target <= startValue 时,只能靠一步步加 1 弥补差距
        return ret + (startValue - target);
    }
};

三、 合并区间:贪心求并集 (Medium)

3.1 题目描述

题目链接56. 合并区间

描述

给定若干个区间,合并所有重叠的区间,返回一个不重叠的区间数组。

3.2 贪心策略与几何证明

贪心策略

  1. 先将所有区间按照左端点从小到大排序。
  2. 遍历区间,维护当前合并块的左右边界 [left, right]
  3. 若新区间的左端点 ≤ \le ≤ 当前 right,说明有重叠,此时贪心地拓展右边界:right = max(right, 新区间的右端点)
  4. 若新区间的左端点 > > > 当前 right,说明断开了,将旧块存入结果,开启新块。

严格证明

排序后,对于任意三个区间 I 1 , I 2 , I 3 I_1, I_2, I_3 I1,I2,I3,其左端点满足 L 1 ≤ L 2 ≤ L 3 L_1 \le L_2 \le L_3 L1≤L2≤L3。

如果 I 1 I_1 I1 与 I 2 I_2 I2 重叠(即 L 2 ≤ R 1 L_2 \le R_1 L2≤R1),那么它们合并后的新右端点为 R n e w = max ⁡ ( R 1 , R 2 ) R_{new} = \max(R_1, R_2) Rnew=max(R1,R2)。

接下来考察 I 3 I_3 I3:由于已经排序,我们确信 L 3 L_3 L3 绝不可能跑到 L 1 L_1 L1 前面去。所以 I 3 I_3 I3 是否能融入前面的大团体,仅仅取决于 L 3 L_3 L3 是否 ≤ R n e w \le R_{new} ≤Rnew。

这证明了,我们只需在一条直线上贪心地维护"目前所能延伸到的最远右端点",无需回头去管左端点,就能不漏掉任何一个重叠块。

3.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        if (intervals.empty()) return {};

        // 1. 按照左端点进行升序排序
        sort(intervals.begin(), intervals.end());

        vector<vector<int>> ret;
        // 2. 初始化第一个合并块的边界
        int left = intervals[0][0];
        int right = intervals[0][1];

        for (int i = 1; i < intervals.size(); i++) {
            int a = intervals[i][0];
            int b = intervals[i][1];

            if (a <= right) {
                // 有重叠,贪心求并集:谁的右边界大就听谁的
                right = max(right, b);
            } else {
                // 彻底断开,结算上一个块
                ret.push_back({left, right});
                // 开启新的征程
                left = a;
                right = b;
            }
        }
        // 循环结束时,最后一个块还在手里,记得交上去
        ret.push_back({left, right});
        
        return ret;
    }
};

四、 无重叠区间:保留生存空间 (Medium)

4.1 题目描述

题目链接435. 无重叠区间

描述

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

4.2 贪心策略与留白证明(面试极易错)

这题与上一题极其相似,但核心逻辑完全相反

上一题求合并,我们希望右边界 right 越好,以吞噬更多区间。

这题求保留最多不重叠区间,发生冲突时,我们希望右边界 right越好!

贪心策略

  1. 同样按左端点排序。
  2. 若遇到重叠区间,贪心地保留右端点较小的那个 (即令 right = min(right, 当前右边界)),并把另一个大的删掉(计数器 +1)。

严格数学证明(替换论证)

假设区间 A A A 和区间 B B B 发生重叠,且 R A < R B R_A < R_B RA<RB。由于必须删掉一个,我们删谁?

假设存在某种最优解 S o p t S_{opt} Sopt,它保留了跨度极广的 B B B,而删除了 A A A。

由于 R A < R B R_A < R_B RA<RB, A A A 占据的时间段是 B B B 占据时间段的"更早结束版"。

如果我们在 S o p t S_{opt} Sopt 中强行用 A A A 替换掉 B B B。因为 A A A 结束得比 B B B 更早,它为后续区间留出了更大的空闲时间(更大的生存空间)

原本能跟在 B B B 后面的合法区间,必定也能跟在 A A A 后面。替换后绝不会导致已有后续区间的冲突。
结论:保留右端点小的区间,是绝对的占优策略。

生活常识辅助

今天有两个会议时间冲突了,一个开到中午 12 点,一个要开到晚上 8 点。你为了下午能安排更多别的会议,肯定果断取消那个开到晚上 8 点的冗长会议。

4.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if (intervals.empty()) return 0;

        // 1. 依然按照左端点排序
        sort(intervals.begin(), intervals.end());

        int ret = 0; // 记录被无情裁掉的区间数量
        int right = intervals[0][1]; // 记录当前安全区的右边界

        for (int i = 1; i < intervals.size(); i++) {
            int a = intervals[i][0];
            int b = intervals[i][1];

            if (a < right) {
                // 冲突了!必须干掉一个。
                ret++; 
                // 贪心核心:干掉右端点大的,保留右端点小的!为后面的兄弟腾出空间!
                right = min(right, b);
            } else {
                // 没有冲突,天下太平,边界平移到当前区间的末尾
                right = b;
            }
        }
        
        return ret;
    }
};

五、 用最少数量的箭引爆气球 (Medium)

5.1 题目描述

题目链接452. 用最少数量的箭引爆气球

描述

气球在 X 轴上占据一定区间。一支箭从某处垂直射上天,能射爆该位置经过的所有气球。求引爆所有气球最少的箭数。

5.2 贪心策略与交集下界证明

这题其实就是求交集

我们要发挥一支箭的极致性价比,就必须让它穿过尽可能多气球的"公共交集"。

贪心策略

  1. 按左端点排序。
  2. 维护一组气球的公共交集右边界 right
  3. 只要新气球的左端点 ≤ \le ≤ right,说明它可以被上一支箭顺带射爆,此时更新公共交集:right = min(right, 新气球右端点)
  4. 如果新气球的左端点 > > > right,说明彻底够不着了,必须消耗一支新箭!

严格数学证明

设当前正在考量的一组重叠气球的公共交集为 [ L c o m m o n , R c o m m o n ] [L_{common}, R_{common}] [Lcommon,Rcommon]。

一支箭要想射爆这一组所有气球,射击点 X X X 必须满足 X ≤ R c o m m o n X \le R_{common} X≤Rcommon。

当加入一个新气球 I k = [ L k , R k ] I_k = [L_k, R_k] Ik=[Lk,Rk] 时,如果 L k ≤ R c o m m o n L_k \le R_{common} Lk≤Rcommon,说明它与当前组有交集,新的公共交集必然被进一步压缩,右边界更新为 min ⁡ ( R c o m m o n , R k ) \min(R_{common}, R_k) min(Rcommon,Rk)。

这确保了只要我们还没超出最小的那个右边界,我们就依然有"一箭多雕"的余地。一旦新气球的起点跨过了这个极限边界,就从几何物理上证明了不存在任何一个 X X X 能同时贯穿它们,必须追加一支箭。

5.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;

        // 1. 按照左端点排序
        sort(points.begin(), points.end());

        int ret = 1; // 至少需要 1 支箭
        int right = points[0][1]; // 记录当前能"一箭多雕"的极限右边界

        for (int i = 1; i < points.size(); i++) {
            int a = points[i][0];
            int b = points[i][1];

            if (a <= right) {
                // 有重叠,说明这只箭还能顺带射爆这个新气球!
                // 但是!为了确保箭能射中所有涉及的气球,公共射击区域变窄了
                right = min(right, b);
            } else {
                // 够不着了,之前的气球必须射爆,消耗一支新箭,并更新射击边界
                ret++;
                right = b;
            }
        }
        
        return ret;
    }
};

六、 整数替换:二进制掩码的最优消除 (Medium)

6.1 题目描述

题目链接397. 整数替换

描述

偶数:变为 n / 2 n / 2 n/2。

奇数:变为 n + 1 n + 1 n+1 或 n − 1 n - 1 n−1。

求变到 1 的最小次数。

6.2 贪心策略与二进制证明

这题如果用递归加备忘录也能做,但贪心法 可以做到极致的 O ( log ⁡ n ) O(\log n) O(logn) 且不需要额外空间。

贪心策略

目标是让数变小,除以 2(右移一位)降维最快。所以我们要尽可能创造偶数,确切地说,我们要创造能被 4 整除的偶数 (即二进制末尾为 00),因为这样能连除两次!

对于奇数,它的二进制末尾必然是 1

  • 情形一:末尾是 01(即 n n % 4 == 1 n)
    我们减 1,末尾变成 00,接下来可以直接右移两次。
  • 情形二:末尾是 11(即 n n % 4 == 3 n)
    我们加 1,发生进位!末尾 11 变成 100(甚至带来连锁进位消去更多的 1)。这显然比减 1 变成 10 更划算。
  • 特例:数字 3
    3 的二进制是 11。如果加 1 变 4,然后 2 然后 1(需 3 步)。如果减 1 变 2,然后 1(只需 2 步)。所以 3 是唯一的例外,应直接减 1。

严格数学证明

对于后缀为 ...01 的奇数:

  • 若 + 1    ⟹    ... 10 +1 \implies \dots 10 +1⟹...10,除以 2 后得 ... 1 \dots 1 ...1(又是奇数)。
  • 若 − 1    ⟹    ... 00 -1 \implies \dots 00 −1⟹...00,除以 2 后得 ... 0 \dots 0 ...0(还是偶数,可继续除以 2)。
    显然 − 1 -1 −1 占优。
    对于后缀为 ...11 的奇数:
  • 若 + 1    ⟹    ... 00 +1 \implies \dots 00 +1⟹...00 甚至引发更高位进位(将一连串的 1 化为 0),极大加速了位数下降。
  • 若 − 1    ⟹    ... 10 -1 \implies \dots 10 −1⟹...10,除以 2 后得 ... 1 \dots 1 ...1。
    显然 + 1 +1 +1 占优(除边界值 3 以外)。

6.3 C++ 代码实战

(注意:当 n 为 2 31 − 1 2^{31}-1 231−1 时,加 1 会发生整型溢出,需要用 long long 强转,或者除法时做变通)

cpp 复制代码
class Solution {
public:
    int integerReplacement(int n) {
        long long num = n; // 防止极限用例 2147483647 加 1 导致溢出
        int ret = 0;
        
        while (num > 1) {
            if (num % 2 == 0) {
                // 偶数无脑除以 2
                num /= 2;
                ret++;
            } else {
                // 奇数分类讨论
                if (num == 3) {
                    // 特判 3
                    num = 1;
                    ret += 2; // 3 -> 2 -> 1
                } else if (num % 4 == 1) {
                    // 二进制末尾为 01,减 1 更优
                    num /= 2; // 相当于 num-1 后除以 2
                    ret += 2; // 包含了减 1 和除以 2 两个步骤
                } else {
                    // 二进制末尾为 11,加 1 引发进位更优
                    num = num / 2 + 1; // 相当于 num+1 后除以 2
                    ret += 2;
                }
            }
        }
        
        return ret;
    }
};

七、 总结

💬 复盘 :区间覆盖和数学位运算,是贪心算法中最能体现代码精简之美的领域。

我们总结出了两套绝对经典的结论:

  1. 区间求并集(合并) :贪心拓展,维护最大右端点
  2. 区间求交集/求留白(无重叠、引爆气球) :贪心收缩,维护最小右端点

下一篇,我们将迎来贪心专题的最终章(Part 6)

我们将迎战俄罗斯套娃信封、被三整除的最大和、距离相等的条形码等终极 Boss 题,见证二维排序降维打击和哈希构造的绝妙手法。千万别错过这收官之战! 🚀

相关推荐
aaa7871 小时前
Codeforces Round 1086 (Div. 2) 题解
算法
Flying pigs~~2 小时前
深度学习之人工神经网络总结
人工智能·深度学习·算法·ann·人工神经网络
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践3 kicad routing tools 从PCB文件获取了哪些信息
算法·agent·pcb·eda·routing
2401_898075122 小时前
代码生成器优化策略
开发语言·c++·算法
郝学胜-神的一滴2 小时前
人工智能发展漫谈:从专家系统到AIGC,再探深度学习核心与Pytorch入门
人工智能·pytorch·python·深度学习·算法·cnn·aigc
知无不研2 小时前
共享内存(Shared Memory)深度全解:Linux高性能IPC的核心机制与实战
linux·运维·c++·共享内存·共享内存与互斥锁
nananaij2 小时前
【LeetCode-03 判断根结点是否等于子结点之和 python解法】
python·算法·leetcode
超级大只老咪2 小时前
差分算法(java)
算法
逆境不可逃2 小时前
【从零入门23种设计模式21】行为型之空对象模式
java·开发语言·数据库·算法·设计模式·职场和发展