前置知识: 对数器 、基础排序、有序表、比较器、堆结构
核心思想
狭义的贪心: 每一步都做出在当前状态下最好或最优的选择,从而希望最终的结果是最好或最优的算法
广义的贪心: 通过分析题目自身的特点和性质,只要发现让求解答案的过程得到加速的结论,都算广义的贪心
贪心是最符合自然智慧的思想,一般分析门槛不高 理解基本的排序、有序结构,有基本的逻辑思维就能理解 但是贪心的题目,千题千面,极难把握 难度在于证明局部最优可以得到全局最优,好在!我们有对数器!
有关贪心的若干现实 & 提醒
1、不要去纠结严格证明,每个题都去追求严格证明,浪费时间、收益很低,而且千题千面。玄学!
2、一定要掌握用对数器验证的技巧,这是解决贪心问题的关键
3、解法几乎只包含贪心思路的题目,代码量都不大
4、大量累积贪心的经验,重点不是证明,而是题目的特征,以及贪心方式的特征,做好总结方便借鉴
5、关注题目数据量,题目的解可能来自贪心,也很可能不是,如果数据量允许,能不用贪心就不用(稳)
6、贪心在笔试中出现概率不低,但是面试中出现概率较低,原因是 淘汰率 vs 区分度
7、广义的贪心无所不在,可能和别的思路结合,一般都可以通过自然智慧想明白,依然不纠结证明
例题
1、最大数
题目描述: 给定一组非负整数 nums,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。
注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。
示例 1:
输入:nums = [10,2] 输出:"210"
示例 2:
输入:nums = [3,30,34,5,9] 输出:"9534330"
解题思路:
这个题的最后返回结果是字符串,同时它的组合也是按照字符串的组合来的
最后的要求是组成一个最大整数,而按照字符串的角度来看就是组合出一个最大字典序的字符串
而字符串的组合无论是字典序最大还是字典序最小,都不只是将大/小字符排在前面,而是组合后的大/小的排在前面,重点是组合,
因此这里总结这里的贪心策略,是
Arrays.sort(strs,(a,b)->(b+a).compareTo(a+b));, 这是最大字典序,如果想要最小字典序,则为Arrays.sort(strs,(a,b)->(a+b).compareTo(b+a));不需要知道证明,只需要知道这种类型就是这样的一个贪心策略,对数其验证正确即可
代码如下
package main.java.class089;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code01_LargestNumber
* @description: 最大数
* // 给定一组非负整数nums
* // 重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数
* // 测试链接 : https://leetcode.cn/problems/largest-number/
* @author: zs宝
* @create: 2025-10-21 09:51
* @Version 1.0
**/public class Code01_LargestNumber {
// strs中全是非空字符串,要把所有字符串拼接起来,形成字典序最小的结果
// 暴力方法
// 为了验证
// 生成所有可能的排列
// 其中选出字典序最小的结果
public static String way1(String[] strs) {
ArrayList<String> ans = new ArrayList<>();
f(strs, 0, ans);
ans.sort((a, b) -> a.compareTo(b));
return ans.get(0);
}
// 全排列代码,讲解038,常见经典递归过程解析
public static void f(String[] strs, int i, ArrayList<String> ans) {
if (i == strs.length) {
StringBuilder path = new StringBuilder();
for (String s : strs) {
path.append(s);
}
ans.add(path.toString());
} else {
for (int j = i; j < strs.length; j++) {
swap(strs, i, j);
f(strs, i + 1, ans);
swap(strs, i, j);
}
}
}
public static void swap(String[] strs, int i, int j) {
String tmp = strs[i];
strs[i] = strs[j];
strs[j] = tmp;
}
// strs中全是非空字符串,要把所有字符串拼接起来,形成字典序最小的结果
// 正式方法
// 时间复杂度O(n*logn)
public static String way2(String[] strs) {
Arrays.sort(strs, (a, b) -> (a + b).compareTo(b + a));
StringBuilder path = new StringBuilder();
for (int i = 0; i < strs.length; i++) {
path.append(strs[i]);
}
return path.toString();
}
// 为了验证
// 生成长度1~n的随机字符串数组
public static String[] randomStringArray(int n, int m, int v) {
String[] ans = new String[(int) (Math.random() * n) + 1];
for (int i = 0; i < ans.length; i++) {
ans[i] = randomString(m, v);
}
return ans;
}
// 为了验证
// 生成长度1~m,字符种类有v种,随机字符串
public static String randomString(int m, int v) {
int len = (int) (Math.random() * m) + 1;
char[] ans = new char[len];
for (int i = 0; i < len; i++) {
ans[i] = (char) ('a' + (int) (Math.random() * v));
}
return String.valueOf(ans);
}
// 为了验证
// 对数器
public static void main(String[] args) {
int n = 8; // 数组中最多几个字符串
int m = 5; // 字符串长度最大多长
int v = 4; // 字符的种类有几种
int testTimes = 2000;
System.out.println("测试开始");
for (int i = 1; i <= testTimes; i++) {
String[] strs = randomStringArray(n, m, v);
String ans1 = way1(strs);
String ans2 = way2(strs);
if (!ans1.equals(ans2)) {
// 如果出错了
// 可以增加打印行为找到一组出错的例子
// 然后去debug
System.out.println("出错了!");
}
if (i % 100 == 0) {
System.out.println("测试到第" + i + "组");
}
}
System.out.println("测试结束");
}
// 测试链接 : https://leetcode.cn/problems/largest-number/ public String largestNumber(int[] nums) {
int n=nums.length;
//将数值转为字符串
String[]strs=new String[n];
for(int i=0;i<n;i++){
strs[i]=String.valueOf(nums[i]);
}
//排序,按最大字典序来
Arrays.sort(strs,(a,b)->(b+a).compareTo(a+b));
if(strs[0].equals("0")){
return "0";
}
StringBuilder ans=new StringBuilder();
for(int i=0;i<n;i++){
ans.append(strs[i]);
}
return ans.toString();
}
}2、两地调度
题目描述: 公司计划面试 2n 人。给你一个数组 costs ,其中 costs[i] = [aCosti, bCosti] 。第 i 人飞往 a 市的费用为 aCosti ,飞往 b 市的费用为 bCosti 。
返回将每个人都飞到 a 、b 中某座城市的最低费用,要求每个城市都有 n 人抵达。
示例 1:
输入:costs = [[10,20],[30,200],[400,50],[30,20]] 输出:110 解释: 第一个人去 a 市,费用为 10。 第二个人去 a 市,费用为 30。 第三个人去 b 市,费用为 50。 第四个人去 b 市,费用为 20。
最低总费用为 10 + 30 + 50 + 20 = 110,每个城市都有一半的人在面试。
示例 2:
输入:costs = [[259,770],[448,54],[926,667],[184,139],[840,118],[577,469]] 输出:1859
示例 3:
输入:costs = [[515,563],[451,713],[537,709],[343,819],[855,779],[457,60],[650,359],[631,42]] 输出:3086
提示:
2 * n == costs.length2 <= costs.length <= 100costs.length为偶数1 <= aCosti, bCosti <= 1000
解题思路:
记住这种类似场景的贪心
将数组非为两类,一类要多少去某处,一类要多少去另一处,求代价最小的分配
贪心的策略为:
先全部都去某一处A,求出总代价sum,同时构造辅助数组arr,记录每个人去另一处B的代价与去去当前处A代价的差值
将辅助数组排序,题目要求去B处的数量为m, 取arr中前m个的累加和,然后与sum相加
为什么是相加
因为sum=很多a相加,arr中是每一个b-a
现在取了前m个arr的数,这些都是去b相比去a最好的那几个,于是sum=a+b-a=b
这样的过程,减少了区分那些人去A,那些人去B的分开计算,直接一步得到最终结果
代码如下
package main.java.class089;
import java.util.Arrays;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code02_TwoCityScheduling
* @description: 两地调度
* // 公司计划面试2n个人,给定一个数组 costs
* // 其中costs[i]=[aCosti, bCosti]
* // 表示第i人飞往a市的费用为aCosti,飞往b市的费用为bCosti
* // 返回将每个人都飞到a、b中某座城市的最低费用
* // 要求每个城市都有n人抵达
* // 测试链接 : https://leetcode.cn/problems/two-city-scheduling/
* @author: zs宝
* @create: 2025-10-21 10:17
* @Version 1.0
**/public class Code02_TwoCityScheduling {
public int twoCitySchedCost(int[][] costs) {
int n=costs.length;
int[]arr=new int[n];
int sum=0;
//全走a
for(int i=0;i<n;i++){
sum+=costs[i][0];
arr[i]=costs[i][1]-costs[i][0];
}
Arrays.sort(arr);
//一半人去a,一半人去b
//上一轮全走的a,这一轮找出去b更划算的前一半人
for(int i=0;i<n/2;i++){
//注意是加,sum=b-a+a-->就是走的b
sum+=arr[i];
}
return sum;
}
}3、吃掉N个橘子的最少天数
题目描述: 厨房里总共有 n 个橘子,你决定每一天选择如下方式之一吃这些橘子:
吃掉一个橘子。
如果剩余橘子数
n能被 2 整除,那么你可以吃掉n/2个橘子。如果剩余橘子数
n能被 3 整除,那么你可以吃掉2*(n/3)个橘子。
每天你只能从以上 3 种方案中选择一种方案。
请你返回吃掉所有 n 个橘子的最少天数。
示例 1:
输入:n = 10 输出:4 解释:你总共有 10 个橘子。 第 1 天:吃 1 个橘子,剩余橘子数 10 - 1 = 9。 第 2 天:吃 6 个橘子,剩余橘子数 9 - 2(9/3) = 9 - 6 = 3。(9 可以被 3 整除) 第 3 天:吃 2 个橘子,剩余橘子数 3 - 2(3/3) = 3 - 2 = 1。 第 4 天:吃掉最后 1 个橘子,剩余橘子数 1 - 1 = 0。 你需要至少 4 天吃掉 10 个橘子。
示例 2:
输入:n = 6 输出:3 解释:你总共有 6 个橘子。 第 1 天:吃 3 个橘子,剩余橘子数 6 - 6/2 = 6 - 3 = 3。(6 可以被 2 整除) 第 2 天:吃 2 个橘子,剩余橘子数 3 - 2*(3/3) = 3 - 2 = 1。(3 可以被 3 整除) 第 3 天:吃掉剩余 1 个橘子,剩余橘子数 1 - 1 = 0。 你至少需要 3 天吃掉 6 个橘子。
示例 3:
输入:n = 1 输出:1
示例 4:
输入:n = 56 输出:6
提示:
1 <= n <= 2*10^9
解题思路:
运用dp表加速计算,避免重复的计算
吃1个实则是为了辅助后面能够吃一半或者吃2/3,
对于每一个数量的橘子,我们都先用吃1个将其辅助到能够吃一半与吃2/3的程度,比较那一种最佳,然后迭代吃
代码如下:
package main.java.class089;
import java.util.HashMap;
import java.util.Map;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code03_MinimumNumberEatOranges
* @description: 吃掉N个橘子的最少天数
* // 厨房里总共有 n 个橘子,你决定每一天选择如下方式之一吃这些橘子
* // 1)吃掉一个橘子
* // 2) 如果剩余橘子数 n 能被 2 整除,那么你可以吃掉 n/2 个橘子
* // 3) 如果剩余橘子数 n 能被 3 整除,那么你可以吃掉 2*(n/3) 个橘子
* // 每天你只能从以上 3 种方案中选择一种方案
* // 请你返回吃掉所有 n 个橘子的最少天数
* // 测试链接 : https://leetcode.cn/problems/minimum-number-of-days-to-eat-n-oranges/
* @author: zs宝
* @create: 2025-10-21 10:41
* @Version 1.0
**/public class Code03_MinimumNumberEatOranges {
public static Map<Integer,Integer> dp=new HashMap<>();
public static int minDays(int n) {
if(n<=1){
return n;
}
if(dp.containsKey(n)){
return dp.get(n);
}
// 1) 吃掉一个橘子
// 2) 如果n能被2整除,吃掉一半的橘子,剩下一半
// 3) 如果n能被3正数,吃掉三分之二的橘子,剩下三分之一
// 因为方法2)和3),是按比例吃橘子,所以必然会非常快
// 所以,决策如下:
// 可能性1:为了使用2)方法,先把橘子吃成2的整数倍,然后直接干掉一半(1天),剩下的n/2调用递归
// 即,n % 2 + 1 + minDays(n/2)
// 可能性2:为了使用3)方法,先把橘子吃成3的整数倍,然后直接干掉三分之二,剩下的n/3调用递归
// 即,n % 3 + 1 + minDays(n/3)
// 至于方法1),完全是为了这两种可能性服务的,因为能按比例吃,肯定比一个一个吃快(显而易见的贪心)
int ans=Math.min(n%2+1+minDays(n/2),n%3+1+minDays(n/3));
dp.put(n,ans);
return ans;
}
}4、会议室II
题目描述: 给你一个会议时间安排的数组 intervals 每个会议时间都会包括开始和结束的时间intervals[i]=[starti, endi] 返回所需会议室的最小数量
解题思路:
有重复时间段的会议只能拿更多的房间,因此题目的本质就是求最大的重复时间段的数量
其实也就是讲解027,题目2,最多线段重合问题
测试链接 : https://www.nowcoder.com/practice/1ae8d0b6bb4e4bcdbf64ec491f63fc37
代码如下
package main.java.class089;
import java.util.Arrays;
import java.util.PriorityQueue;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code04_MeetingRoomsII
* @description: 会议室II
* // 给你一个会议时间安排的数组 intervals
* // 每个会议时间都会包括开始和结束的时间intervals[i]=[starti, endi]
* // 返回所需会议室的最小数量
* // 测试链接 : https://leetcode.cn/problems/meeting-rooms-ii/
* @author: zs宝
* @create: 2025-10-21 11:05
* @Version 1.0
**/public class Code04_MeetingRoomsII {
public static int minMeetingRooms(int[][] meeting) {
int n=meeting.length;
//以开始时间排序
Arrays.sort(meeting,(a,b)->a[0]-b[0]);
//小根堆
PriorityQueue<Integer> heap=new PriorityQueue<>();
int ans=0;
for(int i=0;i<n;i++){
while(!heap.isEmpty() && heap.peek()<=meeting[i][0]){
heap.poll();
}
heap.add(meeting[i][1]);
ans=Math.max(ans,heap.size());
}
return ans;
}
}5、课程表III
题目描述:
这里有 n 门不同的在线课程,按从 1 到 n 编号。给你一个数组 courses ,其中 courses[i] = [durationi, lastDayi] 表示第 i 门课将会 持续 上 durationi 天课,并且必须在不晚于 lastDayi 的时候完成。
你的学期从第 1 天开始。且不能同时修读两门及两门以上的课程。
返回你最多可以修读的课程数目。
示例 1:
输入:courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]] 输出:3 解释: 这里一共有 4 门课程,但是你最多可以修 3 门: 首先,修第 1 门课,耗费 100 天,在第 100 天完成,在第 101 天开始下门课。 第二,修第 3 门课,耗费 1000 天,在第 1100 天完成,在第 1101 天开始下门课程。 第三,修第 2 门课,耗时 200 天,在第 1300 天完成。 第 4 门课现在不能修,因为将会在第 3300 天完成它,这已经超出了关闭日期。
示例 2:
输入:courses = [[1,2]] 输出:1
示例 3:
输入:courses = [[3,2],[4,3]] 输出:0
提示:
1 <= courses.length <= 1041 <= durationi, lastDayi <= 104
解题思路:
这个题的贪心思路是,按照截至日期排序,越先截止的越先考虑是否完成
若当前时间+持续时间<=截止日期,则选择当前课,并将持续时间放于大根堆中
若当前时间+持续时>截止日期,则对当前课的持续时间与大根堆堆顶元素比较
若当前课持续时间<大根堆堆顶元素,则取出堆顶元素,选择当前课持续时间,加入大根堆中
理由,在当前截止时间上,我能让选择相同课程的数量花费的时间更小,也就意味着我后续有更大可能再多选一门课
看见堆,知道思路,却不知道怎么coding, 我该如何才能弥补这块呢
代码如下
package main.java.class089;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code05_CourseScheduleIII
* @description: 课程表III
* // 这里有n门不同的在线课程,按从1到n编号
* // 给你一个数组courses
* // 其中courses[i]=[durationi, lastDayi]表示第i门课将会持续上durationi天课
* // 并且必须在不晚于lastDayi的时候完成
* // 你的学期从第 1 天开始
* // 且不能同时修读两门及两门以上的课程
* // 返回你最多可以修读的课程数目
* // 测试链接 : https://leetcode.cn/problems/course-schedule-iii/
* @author: zs宝
* @create: 2025-10-21 11:28
* @Version 1.0
**/public class Code05_CourseScheduleIII {
class Solution {
public int scheduleCourse(int[][] courses) {
//以终止时间排序,终止时间越靠前的越先完成
//若有终止时间相同的,优先完成花费时间少的
Arrays.sort(courses,(a,b)->(a[1]-b[1]));
PriorityQueue<Integer> heap=new PriorityQueue<>((a,b)->(b-a));
int time=0;
for(int[] course:courses){
if(course[0]+time<=course[1]){
time+=course[0];
heap.add(course[0]);
}else{
//当当前时间加上耗费时间超过最后期限时
if(!heap.isEmpty() && heap.peek()>course[0]){
time+=course[0]-heap.poll();
heap.add(course[0]);
}
}
}
return heap.size();
}
}
}6、连接棒材的最低费用
题目描述: 连接棒材的最低费用 你有一些长度为正整数的棍子 这些长度以数组sticks的形式给出 sticks[i]是第i个木棍的长度 你可以通过支付x+y的成本将任意两个长度为x和y的棍子连接成一个棍子 你必须连接所有的棍子,直到剩下一个棍子 返回以这种方式将所有给定的棍子连接成一个棍子的最小成本 测试链接 : https://leetcode.cn/problems/minimum-cost-to-connect-sticks/ 测试链接 : https://www.luogu.com.cn/problem/P1090
解题思路:
大名鼎鼎的哈夫曼编码
代码如下
package main.java.class089;
import java.util.PriorityQueue;
/**
* @program: ZuoChengxunAlgorithmClass
* @ClassName Code06_MinimumCostToConnectSticks1
* @description: 连接棒材的最低费用(leetcode测试)
* // 你有一些长度为正整数的棍子
* // 这些长度以数组sticks的形式给出
* // sticks[i]是第i个木棍的长度
* // 你可以通过支付x+y的成本将任意两个长度为x和y的棍子连接成一个棍子
* // 你必须连接所有的棍子,直到剩下一个棍子
* // 返回以这种方式将所有给定的棍子连接成一个棍子的最小成本
* // 测试链接 : https://leetcode.cn/problems/minimum-cost-to-connect-sticks/
* @author: zs宝
* @create: 2025-10-22 09:37
* @Version 1.0
**/public class Code06_MinimumCostToConnectSticks1 {
public static int connectSticks(int[] arr) {
//解提思路:大名鼎鼎的哈夫曼编码
//构建小根堆
PriorityQueue<Integer> heap=new PriorityQueue<>((a,b)->a-b);
for(int i=0;i< arr.length;i++){
heap.add(arr[i]);
}
int cur=0;
int sum=0;
while (heap.size()>1){
cur=heap.poll()+ heap.poll();
sum+=cur;
heap.add(cur);
}
return sum;
}
}