前置知识:
讲解005、042 - 对数器
讲解025、026、027 - 基础排序、有序表、比较器、堆结构
狭义的贪心
每一步都做出在当前状态下最好或最优的选择,从而希望最终的结果是最好或最优的算法
广义的贪心
通过分析题目自身的特点和性质,只要发现让求解答案的过程得到加速的结论,都算广义的贪心
贪心是最符合自然智慧的思想,一般分析门槛不高
理解基本的排序、有序结构,有基本的逻辑思维就能理解
但是贪心的题目,千题千面,极难把握
难度在于证明局部最优可以得到全局最优,好在!我们有对数器!贪心专题2、3,这两节大量使用对数器
有关贪心的若干现实 & 提醒
1,不要去纠结严格证明,每个题都去追求严格证明,浪费时间、收益很低,而且千题千面。玄学!
2,一定要掌握用对数器验证的技巧,这是解决贪心问题的关键
3,解法几乎只包含贪心思路的题目,代码量都不大
4,大量累积贪心的经验,重点不是证明,而是题目的特征,以及贪心方式的特征,做好总结方便借鉴
5,关注题目数据量,题目的解可能来自贪心,也很可能不是,如果数据量允许,能不用贪心就不用(稳)
6,贪心在笔试中出现概率不低,但是面试中出现概率较低,原因是 淘汰率 vs 区分度
7,广义的贪心无所不在,可能和别的思路结合,一般都可以通过自然智慧想明白,依然不纠结证明
题目1:最大数
给定一组非负整数nums
重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数
测试链接 : https://leetcode.cn/problems/largest-number/
贪心:a+b>b+a就是两两组合按从大到小排序
C++代码
class Solution {
public:
string largestNumber(vector<int>& nums) {
string res;
sort(nums.begin(),nums.end(),[](int x,int y){
string a=to_string(x),b=to_string(y);
return a+b>b+a;
});
for(auto x:nums)res+=to_string(x);
//防止"00"这种出现
int k=0;
while(k+1<res.size() && res[k]=='0')k++;
return res.substr(k);//从下标k开始往后截
}
};
Java代码
public static 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 path = new StringBuilder();
for (String s : strs) {
path.append(s);
}
return path.toString();
}
题目2:两地调度
公司计划面试2n个人,给定一个数组 costs
其中costs[i]=[aCosti, bCosti]
表示第i人飞往a市的费用为aCosti,飞往b市的费用为bCosti
返回将每个人都飞到a、b中某座城市的最低费用
要求每个城市都有n人抵达
测试链接 : https://leetcode.cn/problems/two-city-scheduling/
贪心:都去A,再将一半人改为B,让改为B的这部分人改动值最小,排序一下就可以了
C++代码
class Solution {
public:
int twoCitySchedCost(vector<vector<int>>& costs) {
int sum=0;
int n=costs.size();
vector<int> c(n);
for(int i=0;i<costs.size();i++){
c[i]=costs[i][1]-costs[i][0];//前一半去B,B和A的差,直接加
sum+=costs[i][0];
}
sort(c.begin(),c.begin()+n);
for(int i=0;i<n/2;i++)sum+=c[i];
return sum;
}
};
Java代码
public static int twoCitySchedCost(int[][] costs) {
int n = costs.length;
int[] arr = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
arr[i] = costs[i][1] - costs[i][0];
sum += costs[i][0];
}
Arrays.sort(arr);
int m = n / 2;
for (int i = 0; i < m; i++) {
sum += arr[i];
}
return sum;
}
题目3:吃掉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/
贪心:尽可能多吃橘子,按比例吃橘子
C++代码
class Solution {
public:
unordered_map<int,int> h;
int minDays(int n) {
if(n<=1)return n;
if(h[n])return h[n];
int ans=min(n%2+1+minDays(n/2),n%3+1+minDays(n/3));
h[n]=ans;
return ans;
}
};
Java代码
// 所有的答案都填在这个表里
// 这个表对所有的过程共用
public static HashMap<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的整数倍,然后直接干掉一半,剩下的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;
}
- 时间复杂度: l o g 2 n log_2n log2n(2个子问题,最大n/2和n/3)
- 空间复杂度: l o g 2 n log_2n log2n(栈深度+h大小)
题目4:会议室II
给你一个会议时间安排的数组 intervals
每个会议时间都会包括开始和结束的时间intervals[i]=[starti, endi]
返回所需会议室的最小数量
测试链接 : https://leetcode.cn/problems/meeting-rooms-ii/
这题就是讲解027,题目2,最多线段重合问题
测试链接 : https://www.nowcoder.com/practice/1ae8d0b6bb4e4bcdbf64ec491f63fc37
- 线段最大重合数:重合的就加进来
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 天开始
且不能同时修读两门及两门以上的课程
返回你最多可以修读的课程数目
测试链接 : https://leetcode.cn/problems/course-schedule-iii/
public static int scheduleCourse(int[][] courses) {
// 0 : 代价
// 1 : 截止
Arrays.sort(courses, (a, b) -> a[1] - b[1]);
// 大根堆
PriorityQueue<Integer> heap = new PriorityQueue<>((a, b) -> b - a);
int time = 0;
for (int[] c : courses) {
if (time + c[0] <= c[1]) {
heap.add(c[0]);
time += c[0];
} else {
// time + c[0] > c[1]
if (!heap.isEmpty() && heap.peek() > c[0]) {
time += c[0] - heap.poll();
heap.add(c[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
哈夫曼树
public static int connectSticks(int[] arr) {
// 小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
heap.add(arr[i]);
}
int sum = 0;
int cur = 0;
while (heap.size() > 1) {
cur = heap.poll() + heap.poll();
sum += cur;
heap.add(cur);
}
return sum;
}
Java代码
// 连接棒材的最低费用(洛谷测试)
// 你有一些长度为正整数的棍子
// 这些长度以数组sticks的形式给出
// sticks[i]是第i个木棍的长度
// 你可以通过支付x+y的成本将任意两个长度为x和y的棍子连接成一个棍子
// 你必须连接所有的棍子,直到剩下一个棍子
// 返回以这种方式将所有给定的棍子连接成一个棍子的最小成本
// 测试链接 : https://www.luogu.com.cn/problem/P1090
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code06_MinimumCostToConnectSticks2 {
public static int MAXN = 10001;
public static int[] nums = new int[MAXN];
public static int n;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
for (int i = 0; i < n; i++) {
in.nextToken();
nums[i] = (int) in.nval;
}
out.println(minCost());
out.flush();
}
out.flush();
out.close();
br.close();
}
public static int minCost() {
size = 0;
for (int i = 0; i < n; i++) {
add(nums[i]);
}
int sum = 0;
int cur = 0;
while (size > 1) {
cur = pop() + pop();
sum += cur;
add(cur);
}
return sum;
}
// 手写小根堆
public static int[] heap = new int[MAXN];
// 堆的大小
public static int size;
public static void add(int x) {
heap[size] = x;
int i = size++;
while (heap[i] < heap[(i - 1) / 2]) {
swap(i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
public static int pop() {
int ans = heap[0];
swap(0, --size);
int i = 0, l = 1, best;
while (l < size) {
best = l + 1 < size && heap[l + 1] < heap[l] ? l + 1 : l;
best = heap[best] < heap[i] ? best : i;
if (best == i) {
break;
}
swap(i, best);
i = best;
l = i * 2 + 1;
}
return ans;
}
public static void swap(int i, int j) {
int tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
}