
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习中,贪心算法 一直是一个既"简单"又"容易出错"的重要思想.它的核心看似直接:每一步都选择当前最优解,希望最终得到全局最优结果.但真正落到具体题目时,如何判断"当前最优"是否可靠,如何证明贪心策略的正确性,往往才是难点所在.在前几篇内容中,我们已经接触了贪心算法的基本思想和一些典型应用.本篇将继续围绕经典实战题目展开,重点解析五类非常具有代表性的题型:单调递增的数字、坏了的计算器、合并区间、无重叠区间、用最少数量的箭引爆气球.这些题目虽然场景不同,但背后都隐藏着贪心选择的关键逻辑:有的需要从数字结构中寻找调整规律,有的需要反向思考减少操作次数,有的则需要在区间问题中抓住排序与边界选择的本质.通过本文的学习,你将进一步理解贪心算法在不同问题中的应用方式,掌握常见区间类问题的处理套路,并学会如何从题目条件中提炼出合理的贪心策略.希望读完本篇后,你不仅能写出正确代码,更能明白为什么这样的贪心选择是有效的.废话不多说,下面跟着小编的节奏🎵一起去疯狂学习吧!

目录
1.单调递增的数字(OJ题)

解法(贪心):
a. 为了方便处理数中的每一位数字,可以先将整数转换成字符串;
b. 从左往右扫描,找到第一个递减的位置;
c. 从这个位置向前推,推到相同区域的最左端;
d. 该点的值 -1,后面的所有数统一变成 9 .





核心代码
cpp
class Solution
{
public:
//核心函数:输入整数n,返回结果
int monotoneIncreasingDigits(int n)
{
//将数字转换为字符串,方便逐位处理每一位数字
string s = to_string(n);
//定义索引i:用于遍历字符串,m为数字的位数
int i = 0, m = s.size();
//第一步:从左到右遍历,找到第一个【不满足递增】的位置
//循环条件:未遍历到末尾,且当前位 <= 下一位(满足递增)
while(i + 1 < m && s[i] <= s[i + 1]) {
i++; //满足递增就继续向后找
}
//特殊情况:遍历完所有位都满足递增,直接返回原数字n
if(i + 1 == m) {
return n;
}
//第二步:回推处理【连续相同数字】的情况
//例:332 → 第一个递减位是第二个3,回推到第一个3,保证结果正确
while(i - 1 >= 0 && s[i] == s[i - 1]) {
i--; //向左回退,直到相邻数字不相等
}
//第三步:核心操作
//将当前位置的数字减1,破坏递减关系
s[i]--;
//减1之后,当前位置**后面的所有数字都设为9**,保证数字最大
for(int j = i + 1; j < m; j++) {
s[j] = '9';
}
//将处理后的字符串转回整数,返回最终结果
return stoi(s);
}
};
完整测试代码
cpp
#include <iostream>
#include <string>
using namespace std;
class Solution
{
public:
// 核心函数:输入整数 n,返回结果
int monotoneIncreasingDigits(int n)
{
// 将数字转换为字符串,方便逐位处理每一位数字
string s = to_string(n);
// 定义索引 i:用于遍历字符串,m 为数字的位数
int i = 0, m = s.size();
// 第一步:从左到右遍历,找到第一个不满足递增的位置
while (i + 1 < m && s[i] <= s[i + 1])
{
i++;
}
// 特殊情况:遍历完所有位都满足递增,直接返回原数字 n
if (i + 1 == m)
{
return n;
}
// 第二步:回推处理连续相同数字的情况
// 例:332 -> 299
while (i - 1 >= 0 && s[i] == s[i - 1])
{
i--;
}
// 第三步:当前位置数字减 1
s[i]--;
// 当前位置后面的所有数字都设为 9,保证结果最大
for (int j = i + 1; j < m; j++)
{
s[j] = '9';
}
// 将处理后的字符串转回整数
return stoi(s);
}
};
int main()
{
Solution sol;
int testCases[] = {
10, // 结果:9
1234, // 本身单调递增,结果:1234
332, // 结果:299
120, // 结果:119
987, // 结果:899
1110, // 连续相同数字回退,结果:999
1000, // 结果:999
0, // 结果:0
9, // 结果:9
11, // 结果:11
101, // 结果:99
110, // 结果:99
1234321, // 结果:1233999
123454321, // 结果:123449999
999999999, // 本身单调递增,结果:999999999
2147483647 // int 最大值附近测试,结果:1999999999
};
int len = sizeof(testCases) / sizeof(testCases[0]);
for (int i = 0; i < len; i++)
{
int n = testCases[i];
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "输入 n:" << n << endl;
cout << "小于等于 n 的最大单调递增数字:";
cout << sol.monotoneIncreasingDigits(n) << endl;
cout << "------------------------" << endl;
}
return 0;
}

2.坏了的计算器(OJ题)

解法(贪心):
贪心策略:正难则反
当反着来思考的时候,我们发现:
i. 当 end <= begin 的时候,只能执行加法操作;
ii. 当 end > begin 的时候,对于奇数来说,只能执行加法操作;对于偶数来说,最好的方式就是执行除法操作,这样的话,每次的操作都是固定唯一的.





核心代码
cpp
class Solution
{
public:
int brokenCalc(int startValue, int target)
{
//核心思想:正难则反 + 贪心
//正向推导会有分支(乘2/减1),无法直接贪心;反向推导(除2/加1)路径唯一,最优解
int ret = 0; // 记录操作的总次数
//循环:当目标值 > 起始值时,持续反向操作
while(target > startValue)
{
//反向操作1:如果target是偶数,最优选择是除以2(对应正向的乘2操作)
if(target % 2 == 0)
target /= 2;
//反向操作2:如果target是奇数,只能先加1变成偶数(对应正向的减1操作)
else
target += 1;
ret++; //每执行一次反向操作,次数+1
}
//当 target <= startValue 时:
//只能通过【减1】操作从 startValue 到 target,需要的次数为 startValue - target
//总次数 = 反向操作次数 + 剩余减1的次数
return ret + startValue - target;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
int brokenCalc(int startValue, int target)
{
// 核心思想:正难则反 + 贪心
// 正向推导会有分支:乘2 / 减1
// 反向推导更清晰:除2 / 加1
int ret = 0;
// 当 target > startValue 时,持续反向操作
while (target > startValue)
{
// 如果 target 是偶数,反向最优操作是除以 2
if (target % 2 == 0)
target /= 2;
// 如果 target 是奇数,只能先加 1 变成偶数
else
target += 1;
ret++;
}
// 当 target <= startValue 时,只能正向通过减 1 到达 target
return ret + startValue - target;
}
};
int main()
{
Solution sol;
vector<pair<int, int>> testCases = {
{2, 3}, // 2 -> 4 -> 3,结果 2
{5, 8}, // 5 -> 4 -> 8,结果 2
{3, 10}, // 3 -> 6 -> 5 -> 10,结果 3
{10, 1}, // 只能不断减 1,结果 9
{1, 1}, // 已经相等,结果 0
{1, 2}, // 1 -> 2,结果 1
{1, 3}, // 1 -> 2 -> 4 -> 3,结果 3
{4, 16}, // 4 -> 8 -> 16,结果 2
{7, 31}, // 7 -> 8 -> 16 -> 32 -> 31,结果 4
{10, 100}, // 多次反向除2和加1
{1024, 1}, // target 小于 startValue
{1, 1000000000} // 大数据测试
};
for (int i = 0; i < testCases.size(); i++)
{
int startValue = testCases[i].first;
int target = testCases[i].second;
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "startValue = " << startValue << ", target = " << target << endl;
cout << "最少操作次数:";
cout << sol.brokenCalc(startValue, target) << endl;
cout << "------------------------" << endl;
}
return 0;
}

3.合并区间(OJ题)

解法(排序 + 贪心):
贪心策略:
a. 先按照区间的左端点排序:此时我们会发现,能够合并的区间都是连续的;
b. 然后从左往后,按照求并集的方式,合并区间.
如何求并集:
由于区间已经按照左端点排过序了,因此当两个区间合并的时候,合并后的区间:
a. 左端点就是前一个区间的左端点;
b. 右端点就是两者右端点的最大值.





核心代码
cpp
class Solution
{
public:
//输入:二维数组 intervals 表示若干个区间
//输出:合并后的不重叠区间数组
vector<vector<int>> merge(vector<vector<int>>& intervals)
{
//第一步:核心预处理
//按照区间的左端点从小到大排序,这是合并重叠区间的前提
sort(intervals.begin(), intervals.end());
//第二步:初始化合并参数
//用 left、right 记录当前正在合并的区间的左右边界
int left = intervals[0][0], right = intervals[0][1];
vector<vector<int>> ret; //存储最终合并后的结果
//第三步:遍历所有区间,逐个合并
for(int i = 1; i < intervals.size(); i++)
{
//取出当前遍历到的区间的左右端点
int a = intervals[i][0], b = intervals[i][1];
//情况1:当前区间与正在合并的区间有重叠/相邻
if(a <= right)
{
//合并区间:更新右边界为两者的最大值
right = max(right, b);
}
//情况2:两个区间完全不重叠
else
{
//将上一个合并完成的区间存入结果
ret.push_back({left, right});
//更新左右边界为当前区间,开始新的合并
left = a;
right = b;
}
}
//第四步:收尾处理
//循环结束后,最后一个合并好的区间还没加入结果,必须补充
ret.push_back({left, right});
//返回最终合并完成的区间列表
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 输入:二维数组 intervals 表示若干个区间
// 输出:合并后的不重叠区间数组
vector<vector<int>> merge(vector<vector<int>>& intervals)
{
// 边界情况:空数组直接返回空结果
if (intervals.empty())
return {};
// 第一步:按照区间的左端点从小到大排序
sort(intervals.begin(), intervals.end());
// 第二步:初始化合并参数
int left = intervals[0][0], right = intervals[0][1];
vector<vector<int>> ret;
// 第三步:遍历所有区间,逐个合并
for (int i = 1; i < intervals.size(); i++)
{
int a = intervals[i][0], 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;
}
};
void printIntervals(const vector<vector<int>>& intervals)
{
cout << "[";
for (size_t i = 0; i < intervals.size(); i++)
{
cout << "[" << intervals[i][0] << ", " << intervals[i][1] << "]";
if (i != intervals.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<vector<vector<int>>> testCases = {
{{1, 3}, {2, 6}, {8, 10}, {15, 18}}, // 普通重叠
{{1, 4}, {4, 5}}, // 边界相接,也需要合并
{{1, 4}, {0, 4}}, // 包含关系
{{1, 4}, {2, 3}}, // 一个区间完全包含另一个区间
{{1, 2}, {3, 4}, {5, 6}}, // 完全不重叠
{{1, 10}, {2, 3}, {4, 5}, {6, 7}}, // 大区间包含多个小区间
{{5, 6}, {1, 3}, {2, 4}, {8, 10}}, // 无序输入
{{1, 1}}, // 单个区间
{}, // 空数组
{{1, 3}, {2, 6}, {6, 8}, {9, 10}}, // 连续边界重叠
{{-10, -1}, {-5, 0}, {1, 3}}, // 包含负数区间
{{1, 4}, {0, 0}}, // 排序后不重叠
{{1, 5}, {2, 6}, {7, 9}, {8, 10}} // 多组合并
};
for (int i = 0; i < testCases.size(); i++)
{
vector<vector<int>> intervals = testCases[i];
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "原始区间:";
printIntervals(intervals);
cout << endl;
vector<vector<int>> result = sol.merge(intervals);
cout << "合并后区间:";
printIntervals(result);
cout << endl;
cout << "------------------------" << endl;
}
return 0;
}

4.无重叠区间(OJ题)

解法(贪心):
贪心策略:
a. 按照左端点排序;
b. 当两个区间重叠的时候,为了能够在移除某个区间后,保留更多的区间,我们应该把区间范围较大的区间移除.
如何移除区间范围较大的区间:
由于已经按照左端点排序了,因此两个区间重叠的时候,我们应该移除右端点较大的区间.





核心代码
cpp
class Solution
{
public:
//输入:二维数组 intervals 存储所有区间
//输出:需要移除的最少区间数量
int eraseOverlapIntervals(vector<vector<int>>& intervals)
{
//第一步:排序
//按照区间的左端点从小到大排序,为贪心选择做准备
sort(intervals.begin(), intervals.end());
//第二步:初始化变量
int ret = 0; //记录需要移除的区间数量
//记录当前保留的最后一个区间的左右边界
int left = intervals[0][0], right = intervals[0][1];
//第三步:遍历所有区间,判断重叠并处理
for(int i = 1; i < intervals.size(); i++)
{
//取出当前遍历到的区间的左右端点
int a = intervals[i][0], b = intervals[i][1];
//情况1:当前区间与上一个保留的区间**发生重叠**
if(a < right)
{
ret++; //必须移除其中一个区间,计数+1
//贪心核心:保留右端点更小的区间,这样能给后面的区间留出更多空间
right = min(right, b);
}
//情况2:两个区间**不重叠**,无需移除
else
{
//更新当前保留的最后一个区间的右边界
right = b;
}
}
//返回最终需要移除的最少区间数
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 输入:二维数组 intervals 存储所有区间
// 输出:需要移除的最少区间数量
int eraseOverlapIntervals(vector<vector<int>>& intervals)
{
// 边界情况:没有区间,不需要移除
if (intervals.empty())
return 0;
// 第一步:排序
// 按照区间的左端点从小到大排序,为贪心选择做准备
sort(intervals.begin(), intervals.end());
// 第二步:初始化变量
int ret = 0;
// 记录当前保留的最后一个区间的左右边界
int left = intervals[0][0], right = intervals[0][1];
// 第三步:遍历所有区间,判断重叠并处理
for (int i = 1; i < intervals.size(); i++)
{
// 取出当前遍历到的区间的左右端点
int a = intervals[i][0], b = intervals[i][1];
// 情况1:当前区间与上一个保留的区间发生重叠
if (a < right)
{
ret++;
// 贪心核心:保留右端点更小的区间
right = min(right, b);
}
// 情况2:两个区间不重叠
else
{
left = a;
right = b;
}
}
return ret;
}
};
void printIntervals(const vector<vector<int>>& intervals)
{
cout << "[";
for (size_t i = 0; i < intervals.size(); i++)
{
cout << "[" << intervals[i][0] << ", " << intervals[i][1] << "]";
if (i != intervals.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<vector<vector<int>>> testCases = {
{{1, 2}, {2, 3}, {3, 4}, {1, 3}}, // 移除 [1,3],结果 1
{{1, 2}, {1, 2}, {1, 2}}, // 保留一个,移除 2 个
{{1, 2}, {2, 3}}, // 不重叠,结果 0
{{1, 100}, {11, 22}, {1, 11}, {2, 12}}, // 结果 2
{{1, 4}, {2, 3}, {3, 5}}, // 保留 [2,3] 和 [3,5],移除 1 个
{{1, 3}, {2, 4}, {3, 5}, {6, 8}}, // 移除 1 个
{{1, 5}, {2, 6}, {3, 7}, {4, 8}}, // 全部重叠,移除 3 个
{{1, 2}, {3, 4}, {5, 6}}, // 完全不重叠,结果 0
{{-10, -1}, {-5, 0}, {1, 3}}, // 包含负数区间,结果 1
{{1, 2}}, // 单个区间,结果 0
{}, // 空数组,结果 0
{{2, 3}, {1, 2}, {3, 4}, {1, 3}}, // 无序输入,排序后处理
{{1, 2}, {2, 2}, {2, 3}}, // 边界相接不算重叠,结果 0
{{1, 3}, {1, 2}, {2, 3}, {3, 4}} // 保留右端点更小的区间,结果 1
};
for (int i = 0; i < testCases.size(); i++)
{
vector<vector<int>> intervals = testCases[i];
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "原始区间:";
printIntervals(intervals);
cout << endl;
cout << "需要移除的最少区间数量:";
cout << sol.eraseOverlapIntervals(intervals) << endl;
cout << "------------------------" << endl;
}
return 0;
}

5.⽤最少数量的箭引爆⽓球(OJ题)

解法(贪心):
贪心策略:
a. 按照左端点排序,我们发现,排序后有这样一个性质:互相重叠的区间都是连续的;
b. 这样,我们在射箭的时候,要发挥每一支箭最大的作用,应该把互相重叠的区间统一引爆.
如何求互相重叠区间?
由于我们是按照左端点排序的,因此对于两个区间,我们求的是它们的交集:
a. 左端点为两个区间左端点的最大值(但是左端点不会影响我们的合并结果,所以可以忽略);
b. 右端点为两个区间右端点的最小值.





核心代码
cpp
class Solution
{
public:
//输入:二维数组 points 存储所有气球的区间
//输出:引爆所有气球所需的最少弓箭数量
int findMinArrowShots(vector<vector<int>>& points)
{
//第一步:排序
//按照气球区间的左端点从小到大排序,方便贪心遍历
sort(points.begin(), points.end());
//第二步:初始化变量
int right = points[0][1]; //记录当前箭能覆盖的最右侧边界(重叠区间的最小右边界)
int ret = 1; //最少需要1支箭(至少有一个气球)
//第三步:遍历所有气球,贪心选择射箭位置
for(int i = 1; i < points.size(); i++)
{
//取出当前气球的左右端点
int a = points[i][0], b = points[i][1];
//情况1:当前气球与上一支箭的覆盖区间重叠
//可以用同一支箭引爆,更新右边界为最小值(保证所有重叠气球都能被击穿)
if(a <= right)
{
right = min(right, b);
}
//情况2:当前气球与上一支箭的覆盖区间不重叠
//需要新增一支箭,更新右边界为当前气球的右边界
else
{
ret++;
right = b;
}
}
//返回最少需要的箭数
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 输入:二维数组 points 存储所有气球的区间
// 输出:引爆所有气球所需的最少弓箭数量
int findMinArrowShots(vector<vector<int>>& points)
{
// 边界情况:没有气球,不需要箭
if (points.empty())
return 0;
// 第一步:排序
// 按照气球区间的左端点从小到大排序,方便贪心遍历
sort(points.begin(), points.end());
// 第二步:初始化变量
int right = points[0][1]; // 当前箭能覆盖的最右侧边界
int ret = 1; // 至少有一个气球时,最少需要 1 支箭
// 第三步:遍历所有气球,贪心选择射箭位置
for (int i = 1; i < points.size(); i++)
{
int a = points[i][0], b = points[i][1];
// 情况1:当前气球与上一支箭的覆盖区间重叠
if (a <= right)
{
// 更新重叠区间的最小右边界
right = min(right, b);
}
// 情况2:当前气球与上一支箭的覆盖区间不重叠
else
{
ret++;
right = b;
}
}
return ret;
}
};
void printPoints(const vector<vector<int>>& points)
{
cout << "[";
for (size_t i = 0; i < points.size(); i++)
{
cout << "[" << points[i][0] << ", " << points[i][1] << "]";
if (i != points.size() - 1)
cout << ", ";
}
cout << "]";
}
int main()
{
Solution sol;
vector<vector<vector<int>>> testCases = {
{{10, 16}, {2, 8}, {1, 6}, {7, 12}}, // 经典示例,结果 2
{{1, 2}, {3, 4}, {5, 6}, {7, 8}}, // 完全不重叠,结果 4
{{1, 2}, {2, 3}, {3, 4}, {4, 5}}, // 边界相接算重叠,结果 2
{{1, 10}, {2, 3}, {4, 5}, {6, 7}}, // 大区间覆盖多个小区间,结果 3
{{1, 10}, {2, 9}, {3, 8}, {4, 7}}, // 全部重叠,结果 1
{{1, 2}}, // 单个气球,结果 1
{}, // 空数组,结果 0
{{-10, -1}, {-5, 0}, {1, 3}}, // 包含负数区间,结果 2
{{5, 6}, {1, 2}, {2, 5}, {7, 9}}, // 无序输入,结果 2
{{1, 5}, {2, 6}, {7, 9}, {8, 10}}, // 两组重叠区间,结果 2
{{1, 1}, {1, 1}, {1, 1}}, // 点区间完全相同,结果 1
{{1, 1}, {2, 2}, {3, 3}}, // 点区间不重叠,结果 3
{{1, 3}, {3, 3}, {3, 5}}, // 都能在 3 处射穿,结果 1
{{-2147483648, 2147483647}, {0, 0}} // 极值区间,结果 1
};
for (int i = 0; i < testCases.size(); i++)
{
vector<vector<int>> points = testCases[i];
cout << "测试用例 " << i + 1 << ":" << endl;
cout << "气球区间:";
printPoints(points);
cout << endl;
cout << "最少弓箭数量:";
cout << sol.findMinArrowShots(points) << endl;
cout << "------------------------" << endl;
}
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容的更新【贪心算法】(经典实战应用解析(六):整数替换、俄罗斯套娃信封问题、可被三整除的最⼤和、距离相等的条形码、重构字符串)
每日心灵鸡汤: 心有分寸,行有方向!
遇大事要静,遇难事要变,遇烂事要离,遇顺事要敛.人的一生,就像一趟充满未知的旅程,会撞见突如其来的风雨,会邂逅意料之外的坦途,也会碰上纠缠不休的纷扰.我们无法左右事情的走向,却能调整自己面对世事的心态.遇事慌乱,只会乱了方寸,让局面愈发棘手;沉下心来,稳住阵脚,才能在迷雾中找到破局的关键.前路受阻,一味硬抗只会耗尽心力;懂得变通,换个思路,往往能遇到柳暗花明的转机.先改变看待事物的角度,再提升人生的高度.以静制动,以变求通,以离止损,以敛养慧.守好这四颗心,无论前路是晴是雨,都能走得从容,活得坦荡.
