(左程云)算法讲解089【必备】贪心经典题目专题1

前置知识:
讲解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;
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值