【贪心算法】专题(六):降维打击与错位重构的终极收官

文章目录

    • 打破维度的枷锁,重塑规则的秩序
    • [一、 前言:贪心的高级战术](#一、 前言:贪心的高级战术)
    • [二、 俄罗斯套娃信封问题:降维打击的艺术 (Hard)](#二、 俄罗斯套娃信封问题:降维打击的艺术 (Hard))
      • [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++ 代码实战(以重构字符串为例))
    • [五、 贪心算法终局总结:道法自然](#五、 贪心算法终局总结:道法自然)

打破维度的枷锁,重塑规则的秩序

一、 前言:贪心的高级战术

💬 开篇 :如果你觉得二维的 DP 填表太慢,或者是复杂的字符串拼接太乱,那么巧妙的排序 加上特定的数据填充分发就是贪心的终极兵器。

🚀 核心破局点

  1. 降维打击(俄罗斯套娃):二维的两个条件比较,能否通过特定的排序规则,把其中一维锁死,将问题变成一维的最长递增子序列(LIS)?
  2. 反向排除法(可被三整除的最大和):与其正向去挑哪些数字加起来能被 3 整除,不如把所有数字全加上,再根据"余数"抠出损失最小的数字!
  3. 错位插空法(条形码/重构字符串):遇到要求"相邻元素不能相同"时,只要让频率最高的老大去占领偶数位,剩下的顺便填,天下自然太平。

💡 终局宣誓:拿下这最后 4 题,你的贪心与逻辑思维将无人能挡!


二、 俄罗斯套娃信封问题:降维打击的艺术 (Hard)

2.1 题目描述

题目链接354. 俄罗斯套娃信封问题

描述

给定一个二维整数数组 envelopes,其中 envelopes[i] = [w, h] 表示信封的宽度和高度。

当信封 A 的宽和高都 严格大于 信封 B 时,B 才能放进 A 里。

求最多能嵌套多少个信封?

2.2 贪心策略与降维证明(极度震撼)

如果只有一维数据,这就是我们前面讲过的 最长递增子序列 (LIS) 问题。

现在有两个维度(宽和高),如何降维?

贪心排序策略

  1. 我们先按照宽度 w w w 从小到大 排序。这样只要我们从左往右挑,宽度天然就是递增的,我们只用盯着高度 h h h 去求 LIS 就行了!
  2. 致命漏洞 :如果宽度相同呢?比如 [3, 4][3, 5]。宽度不满足"严格大于",但按从小到大排后,高度是 4, 5,会被 LIS 算法误认为可以套娃,从而得出错误答案。
  3. 降维补丁(核心魔法)如果宽度相同,我们让高度 h h h 从大到小降序排列!
    这样排完序后,同宽度的信封群变成了 [3, 5][3, 4]。在提取高度数组时,由于 5 5 5 在 4 4 4 前面,找严格递增子序列时绝对不可能把 5 5 5 和 4 4 4 同时选进去!
    这完美地避开了同宽度信封的互相嵌套。

严格逻辑总结

重写排序规则后,宽度的影响被彻底隔离。此时只提取 h h h 维度形成一维数组,直接使用我们在前面讲过的 O ( N log ⁡ N ) O(N \log N) O(NlogN) 贪心二分 LIS 算法,问题迎刃而解。

2.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        // 1. 制定降维排序法则
        sort(envelopes.begin(), envelopes.end(), [](const vector<int>& a, const vector<int>& b) {
            // 如果宽度不同,按宽度升序;如果宽度相同,按高度降序!
            return a[0] != b[0] ? a[0] < b[0] : a[1] > b[1];
        });

        // 2. 对高度 h 数组求 LIS (最长严格递增子序列)
        vector<int> ret;
        ret.push_back(envelopes[0][1]);

        for (int i = 1; i < envelopes.size(); i++) {
            int h = envelopes[i][1];
            
            if (h > ret.back()) {
                // 如果比当前 LIS 的末尾大,直接接上去
                ret.push_back(h);
            } else {
                // 贪心替换:在 ret 中二分查找第一个大于等于 h 的位置并替换它,降低门槛
                int left = 0, right = ret.size() - 1;
                while (left < right) {
                    int mid = left + (right - left) / 2;
                    if (ret[mid] >= h) {
                        right = mid;
                    } else {
                        left = mid + 1;
                    }
                }
                ret[left] = h;
            }
        }
        
        // 返回 LIS 的长度,即为最多嵌套的信封数
        return ret.size();
    }
};

三、 可被三整除的最大和:反向排除法 (Medium)

3.1 题目描述

题目链接1262. 可被三整除的最大和

描述

给你一个整数数组 nums,请你找出并返回能被三整除的元素最大和

3.2 贪心策略与同余定理推导

贪心策略(正难则反)

把所有数先全部加起来,得到 sum。如果 sum % 3 == 0,这就是完美答案。

如果不等于 0 呢?根据同余定理,我们只能抛弃掉一些元素来填平这个"余数"。我们要抛弃的数值必须尽可能小

分类讨论

sum 的余数情况:

  1. 余数为 1:多出了 1 的余数。我们有两种填平方案:

    • 方案 A:删掉一个 % 3 == 1 的极小值(比如丢掉一个 4)。
    • 方案 B:删掉两个 % 3 == 2 的极小值(比如丢掉 2 和 5,因为 2 + 5 = 7 2+5=7 2+5=7, 7 7%3=1 7,同样能抵消掉多出的 1)。
      贪心选择:计算方案 A 和方案 B 哪个减去的数值更小,就用哪个!
  2. 余数为 2:多出了 2 的余数。我们有两种填平方案:

    • 方案 A:删掉一个 % 3 == 2 的极小值。
    • 方案 B:删掉两个 % 3 == 1 的极小值。
      同样,取减去代价最小的方案。

前期准备

在遍历数组累加时,顺手记录下 %3==1 的最小和次小值( x 1 , x 2 x_1, x_2 x1,x2),以及 %3==2 的最小和次小值( y 1 , y 2 y_1, y_2 y1,y2)。

3.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int maxSumDivThree(vector<int>& nums) {
        // 定义一个足够大的数,注意别让相加时整数溢出
        const int INF = 0x3f3f3f3f; 
        
        int sum = 0;
        int x1 = INF, x2 = INF; // 存储余数为 1 的最小值和次小值
        int y1 = INF, y2 = INF; // 存储余数为 2 的最小值和次小值

        for (int x : nums) {
            sum += x;
            if (x % 3 == 1) {
                if (x < x1) { x2 = x1; x1 = x; }
                else if (x < x2) { x2 = x; }
            } 
            else if (x % 3 == 2) {
                if (x < y1) { y2 = y1; y1 = x; }
                else if (x < y2) { y2 = x; }
            }
        }

        // 极值分类裁决
        if (sum % 3 == 0) {
            return sum;
        } 
        else if (sum % 3 == 1) {
            // 挑损失最小的:删一个余1的,或者删两个余2的
            return max(sum - x1, sum - y1 - y2);
        } 
        else {
            // 余数为 2:挑损失最小的:删一个余2的,或者删两个余1的
            return max(sum - y1, sum - x1 - x2);
        }
    }
};

四、 距离相等的条形码 / 重构字符串:错位插空的巅峰双子星 (Medium)

这两道题不仅逻辑完全一致,连解题模型和坑点都一模一样。我们就放在一起讲解。

4.1 题目描述

题 11054. 距离相等的条形码 重新排列数字,使相邻两个数字不同。
题 2767. 重构字符串 重新排列小写字母,使相邻字母不同。

4.2 贪心策略与插空排布图解

为什么排着排着,最后总是会出现相邻元素相同?
元凶是那些"出现频率极其高"的霸道元素!如果你前期不安排它们,把它们挤到最后,它们肯定会撞车。

贪心策略(插空法)

  1. 统计频率 :找出出现频率最高的那个王牌元素(设频次为 maxCount)。
  2. 合法性特判(重构字符串题专属) :如果王牌元素的出现次数大于数组总长度的一半向上取整 (n+1)/2,绝对不可能合法排开,直接返回空字符串。(条形码题题目保证有解,可略过此步)。
  3. 优先安置王牌 :把王牌元素填入索引为偶数的位置(0, 2, 4, ...)。
  4. 见缝插针 :把剩下的小弟元素继续往后面的偶数位填;如果偶数位到底了(越界了),就跳回下标 1(奇数位),继续隔一个填一个。

ASCII 插空连环画

bash 复制代码
假设我们要排:A A A A B B C (n=7, maxCount=4, maxChar=A)
奇偶位索引:0  1  2  3  4  5  6
1. 安排 A:A     A     A     A  (偶数位全占满,极其安全)
2. 安排 B:A  B  A  B  A     A  (跳到奇数位 index=1 继续)
3. 安排 C:A  B  A  B  A  C  A  (完美交错!)

4.3 C++ 代码实战(以重构字符串为例)

cpp 复制代码
class Solution {
public:
    string reorganizeString(string s) {
        int hash[26] = {0}; // 哈希表统计字频
        char maxChar = ' ';
        int maxCount = 0;
        
        // 1. 揪出那个出现频率最高的"霸主"
        for (char ch : s) {
            hash[ch - 'a']++;
            if (hash[ch - 'a'] > maxCount) {
                maxCount = hash[ch - 'a'];
                maxChar = ch;
            }
        }
        
        int n = s.size();
        
        // 2. 合法性生死劫:如果霸主多到连一半的隔间都住不下,注定无解
        if (maxCount > (n + 1) / 2) return "";
        
        string ret(n, ' ');
        int index = 0; // 从 0 索引出发
        
        // 3. 霸主先行:填满前面所有的偶数位置
        for (int i = 0; i < maxCount; i++) {
            ret[index] = maxChar;
            index += 2;
        }
        
        // 霸主的兵力耗尽,抹除记录
        hash[maxChar - 'a'] = 0;
        
        // 4. 残兵列阵:见缝插针
        for (int i = 0; i < 26; i++) {
            while (hash[i] > 0) {
                // 如果偶数位已经突破边界,立刻折返去抢占奇数位 1
                if (index >= n) index = 1;
                
                ret[index] = 'a' + i;
                index += 2;
                hash[i]--;
            }
        }
        
        return ret;
    }
};

(注:距离相等的条形码这道题的代码逻辑与此几乎 100% 一致,只是将处理字符的 char 和数组变成了 int 以及利用 unordered_map 处理数字哈希,不再赘述。)


五、 贪心算法终局总结:道法自然

💬 结业致辞:这是《贪心算法专题》的最终篇,也是我们这一路长途跋涉的心法总结。

从第一篇的柠檬水找零 ,到现在的错位插空法 ,贪心算法从来都没有一套能"死记硬背"的通用模板,它考察的是真正的底层逻辑与数学推演能力

但我们依然可以从这 30 道精选题目中提炼出应对所有贪心题的四大名捕

  1. 极值策略法:当面临资源紧缺或需要减半时,永远拿最极端的数字(最大/最小)开刀。
  2. 排序降维法 :如果数组乱序无法下手, 不要犹豫,排序! 如果是多维干涉(如套娃信封),就用规则排序锁定一维。
  3. 反向思维法:走正路走出了无数分支(比如坏掉的计算器或余数问题),立刻回头看逆推,世界往往无比清爽。
  4. 区间交并法:交集负责爆破(射气球),并集负责合并,留白负责生存。统一左端点排序是打开这扇门唯一的钥匙。

🎊 恭喜通关! 希望这套算法专题能化作你键盘上飞舞的代码,在笔试和面试中所向披靡,斩获最高薪的 Offer!

江湖路远,保持热爱,我们后会有期!🚀✨

相关推荐
2301_800895101 小时前
dijkstra求最短路径--备考蓝桥杯版
算法
葡萄9891 小时前
蓝桥杯k倍区间(前缀和、余数统计)
数据结构·算法
智者知已应修善业2 小时前
【任何一个自然数m的立方均可写成m个连续奇数之和】2024-10-17
c语言·数据结构·c++·经验分享·笔记·算法
YYYing.2 小时前
【Linux/C++多线程篇(二) 】给线程装上“红绿灯”:通俗易懂的同步互斥机制讲解 & C++ 11下的多线程
linux·c语言·c++·经验分享·ubuntu
程序员爱钓鱼2 小时前
Go字符串与数值转换核心库: strconv深度解析
后端·面试·go
阿里嘎多哈基米2 小时前
速通Hot100-Day07——栈
数据结构·算法·leetcode··队列·hot100
一叶落4382 小时前
LeetCode 135. 分发糖果(C语言)| 贪心算法 + 双向遍历详解
c语言·数据结构·算法·leetcode·贪心算法·哈希算法
2401_900151542 小时前
自定义异常类设计
开发语言·c++·算法
努力学算法的蒟蒻2 小时前
day113(3.15)——leetcode面试经典150
算法·leetcode·职场和发展