文章目录
-
- 打破维度的枷锁,重塑规则的秩序
- [一、 前言:贪心的高级战术](#一、 前言:贪心的高级战术)
- [二、 俄罗斯套娃信封问题:降维打击的艺术 (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 填表太慢,或者是复杂的字符串拼接太乱,那么巧妙的排序 加上特定的数据填充分发就是贪心的终极兵器。
🚀 核心破局点:
- 降维打击(俄罗斯套娃):二维的两个条件比较,能否通过特定的排序规则,把其中一维锁死,将问题变成一维的最长递增子序列(LIS)?
- 反向排除法(可被三整除的最大和):与其正向去挑哪些数字加起来能被 3 整除,不如把所有数字全加上,再根据"余数"抠出损失最小的数字!
- 错位插空法(条形码/重构字符串):遇到要求"相邻元素不能相同"时,只要让频率最高的老大去占领偶数位,剩下的顺便填,天下自然太平。
💡 终局宣誓:拿下这最后 4 题,你的贪心与逻辑思维将无人能挡!
二、 俄罗斯套娃信封问题:降维打击的艺术 (Hard)
2.1 题目描述
题目链接 :354. 俄罗斯套娃信封问题
描述 :
给定一个二维整数数组
envelopes,其中envelopes[i] = [w, h]表示信封的宽度和高度。当信封 A 的宽和高都 严格大于 信封 B 时,B 才能放进 A 里。
求最多能嵌套多少个信封?
2.2 贪心策略与降维证明(极度震撼)
如果只有一维数据,这就是我们前面讲过的 最长递增子序列 (LIS) 问题。
现在有两个维度(宽和高),如何降维?
贪心排序策略:
- 我们先按照宽度 w w w 从小到大 排序。这样只要我们从左往右挑,宽度天然就是递增的,我们只用盯着高度 h h h 去求 LIS 就行了!
- 致命漏洞 :如果宽度相同呢?比如
[3, 4]和[3, 5]。宽度不满足"严格大于",但按从小到大排后,高度是4, 5,会被 LIS 算法误认为可以套娃,从而得出错误答案。 - 降维补丁(核心魔法) : 如果宽度相同,我们让高度 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 的余数。我们有两种填平方案:
- 方案 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 哪个减去的数值更小,就用哪个!
- 方案 A:删掉一个
-
余数为 2:多出了 2 的余数。我们有两种填平方案:
- 方案 A:删掉一个
% 3 == 2的极小值。 - 方案 B:删掉两个
% 3 == 1的极小值。
同样,取减去代价最小的方案。
- 方案 A:删掉一个
前期准备 :
在遍历数组累加时,顺手记录下 %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 题目描述
题 1 :1054. 距离相等的条形码 重新排列数字,使相邻两个数字不同。
题 2 :767. 重构字符串 重新排列小写字母,使相邻字母不同。
4.2 贪心策略与插空排布图解
为什么排着排着,最后总是会出现相邻元素相同?
元凶是那些"出现频率极其高"的霸道元素!如果你前期不安排它们,把它们挤到最后,它们肯定会撞车。
贪心策略(插空法):
- 统计频率 :找出出现频率最高的那个王牌元素(设频次为
maxCount)。 - 合法性特判(重构字符串题专属) :如果王牌元素的出现次数大于数组总长度的一半向上取整
(n+1)/2,绝对不可能合法排开,直接返回空字符串。(条形码题题目保证有解,可略过此步)。 - 优先安置王牌 :把王牌元素填入索引为偶数的位置(
0, 2, 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 道精选题目中提炼出应对所有贪心题的四大名捕:
- 极值策略法:当面临资源紧缺或需要减半时,永远拿最极端的数字(最大/最小)开刀。
- 排序降维法 :如果数组乱序无法下手, 不要犹豫,排序! 如果是多维干涉(如套娃信封),就用规则排序锁定一维。
- 反向思维法:走正路走出了无数分支(比如坏掉的计算器或余数问题),立刻回头看逆推,世界往往无比清爽。
- 区间交并法:交集负责爆破(射气球),并集负责合并,留白负责生存。统一左端点排序是打开这扇门唯一的钥匙。
🎊 恭喜通关! 希望这套算法专题能化作你键盘上飞舞的代码,在笔试和面试中所向披靡,斩获最高薪的 Offer!
江湖路远,保持热爱,我们后会有期!🚀✨