贪心算法
- 贪心算法的解题过程
- 贪心算法案例
- 1.选择排序
- 2. 平衡字符串
- 3. 买卖股票的最佳时机 II
- 4. 跳跃游戏
- 5 钱币找零
- 6 多机调度问题
- 7.活动选择
- 8. 最多可以参加的会议数目
- 9. 无重叠区间
来自算法导论对于这个贪心算法的解释定义
贪心算法(又名贪婪算法)故名思意就是一个“贪心”的算法。把一个问题拆分成子问题,贪心算法只顾子问题的最优解,不考虑问题的整体,只考虑当下,这就是所谓的贪心。
贪心算法的解题过程
- 建立数学模型
- 把求解的问题拆分成若干个若子问题;
- 对每一子问题求解最优解;
- 把子问题的最优解合并成该问题的解
该算法只能满足约束条件下可行范围的解,最终解不一定是这个问题的最优解。
贪心算法案例
1.选择排序
选择排序(排升序)是在数据结构学习中学到的几个排序算法之一,该排序算法只考虑在未排序范围内的最小值放在前面,最后未排序范围变成了0,排序算法终止。这个只考虑部分最小,不考虑总体这个就是贪心思想。
public static void main(String[] args) {
int[] arr = {9,8,4,6,2,5};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void selectSort(int[] arr) {
//从下标0开始一直缩小未排序数据的范围,
for (int i = 0; i < arr.length; i++) {
//找到未排序数据中的最小值的索引。
int min = i;
for (int j = i; j < arr.length; j++) {
if (arr[j] < arr[min]){
min = j;
}
}
//最小值和未排序数据最小下标的值进行互换
swap(arr,min,i);
}
}
private static void swap(int[] arr, int min, int i) {
int tmp = arr[min];
arr[min] = arr[i];
arr[i] = tmp;
}
2. 平衡字符串
原题链接
✨问题描述:
平衡字符串 中,‘L’ 和 ‘R’ 字符的数量是相同的。
给你一个平衡字符串 s,请你将它分割成尽可能多的子字符串,并满足:
每个子字符串都是平衡字符串。
返回可以通过分割得到的平衡字符串的 最大数量 。
✨案例:
输入:s = “RLRRLLRLRL” 输出:4 解释:s 可以分割为 “RL”、“RRLL”、“RL”、“RL”
,每个子字符串中都包含相同数量的 ‘L’ 和 ‘R’ 。
✨题目分析:
该题目是分割成一个一个的平衡字符串,要求平衡字符串的个数最多。
根据案例1来说:我们如果找到了一个平衡子串,RL但是我们还要往后进行不重新找,找到RLLRRLL,剩下的字符串最多只能找到两个平衡串。所以我们不能够让子串串嵌套。只要找到一个平衡字串就重新找字串。这是一个很明显的贪心算法思想
所以贪心思想:不能够让字串嵌套。
我们可以定义一个计数变量count来记录平衡串的个数,
判断平衡串:可以定义变量balance,如果是R进行+1,如果是L进行-1.什么时候balance == 0,就说明该子串为平衡串。
✨实现代码:
class Solution {
public int balancedStringSplit(String s) {
int balance = 0;
int count = 0;
for(int i = 0;i < s.length();i++){
if(s.charAt(i) == 'R'){
balance++;
}else if(s.charAt(i) == 'L'){
balance--;
}
if(balance == 0){
count++;
}
}
return count;
}
}
3. 买卖股票的最佳时机 II
原题链接
✨题目描述:
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
✨案例:
输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 =
5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 => 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。
✨题目分析:
该题目属于一个马后炮分析股票,我们在买股票的时候怎么会知道该股票第二天是涨还是跌呢。我们在做这个题目的时候就在股票上升阶段我们在第一题卖入,明天要下跌了我么就把股票给卖出去。在股票下跌阶段我们不进行任何的一个操纵。此种操作才会使我们的这个股票收益最大。
现在我们的难题就是对上升阶段进行操作,如何在上升阶段的最后一天把股票卖了,如果我们只考虑起始时间和终止时间我们会把最后一天分成两种情况是否是股票交易的最后一天还仅仅是股票上涨的最后一天。这种明显就比较复杂。我们可以考虑贪心思想,只考虑局部不考虑整体,比如只要是明天股票上升我今天就买,明天就买。这种和上升阶段的最后一天 - 上升阶段的第一天 得到的数值是相同的。
贪心思想:只要是明天上升我今天就买明天卖。只要是下降我就不进行任何操作。
✨代码实现:
class Solution {
public int maxProfit(int[] prices) {
int money = 0;
for(int i = 1;i < prices.length;i++){
if(prices[i] > prices[i - 1]){
money += prices[i] - prices[i-1];
}
}
return money;
}
}
✨其他思路:
这个问题还可以用动态规划来进行解决。我们可以定义一个二维数组dp[i][j]。i 代表天数,j代表改天持有股票还是持有现金。j == 0 是持有现金。 j == 1 就是持有股票。一天只有这两种状态。这一天持有现金是可能昨天持有现金今天没有任何操作,还有一种可能就是昨天持有股票今天把股票给卖了。这一天持有股票肯能是前一天有股票今天没买,也有可能是昨天没有股票,今天买了。
- 装填转移:
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices) //前者昨天没股票今天没进行操作。后者是昨天买了股票今天把股票买了。取两者最大值。
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices)//前者是昨天的股票今天没有进行任何操作。后者是昨天没有股票今天买了股票。两者取最大值。 - 初始状态:
dp[0][0] = 0;//第一天没有买股票
dp[0][1] = -7; // 第一天持有股票现有现金为 -7. - 返回结果:
return dp[prices.length - 1][0];//肯定是最后一天把股票卖出去了,手里的现金最多。
就案例一来进行画图解析
代码:
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
//进行初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
//进行状态转移
for(int i = 1;i < len;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);//没有股票
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices[i]);//持有股票
}
return dp[len - 1][0];
}
}
当然这个动态规划可以进一步的简化,但是是比贪心算法要复杂,所以这个题用动态规划属于是杀鸡用来宰牛刀。
4. 跳跃游戏
原题链接
✨题目描述:
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
✨案例:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3
步到达最后一个下标。
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 ,
所以永远不可能到达最后一个下标。
✨题目分析
设想一下,对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置x,它本身可以到达,并且它跳跃的最大长度为 x + nums[x],这个值大于等于 y,即 x+nums[x] ≥ y,那么位置y也可以到达。换句话说,对于每一个可以到达的位置 x,它使得 x+1, x+2,⋯,x+nums[x] 这些连续的位置都可以到达。 这样以来,我们依次遍历数组中的每一个位置,并实时维护最远可以到达的位置。
对于当前遍历到的位置x,如果它在最远可以到达的位置的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x]更新 最远可以到达的位置。
在遍历的过程中,如果最远可以到达的位置大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回True作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回
False 作为答案。
就以案例1来说,0下标最远可以到达2下标,1下标最远可以到达4下标,4下标就是最后一个位置,
案例2进行分析:0下标最远可以到达3下标,1下标最远可以到达3下标,2下标最远可以到达3下标,3位置最远可以到达3下标,但是4下标就不能能够到达了。
我们可以遍历数组然后用max记录能到达的最远位置,就是用max和该x+nums[x],记录最大值。如果在中间某个位置跳跃的位置包含,我们就可以返回true了。如果中间某个位置跳跃不到或者我们循环结束到打不了最后位置我们就返回false。
贪心思想:站在每一个位置,更新可以到达的最远位置。
✨ 题目代码
class Solution {
public boolean canJump(int[] nums) {
int max = 0;
int pos = nums.length - 1;
//遍历数组记录能到达的最远值
for(int i = 0;i <= pos;i++){
if(max >= i){//能到达该位置
max = Math.max(max,nums[i] + i);
if(max >= pos){//能跳跃到的最远距离包含了目标位置
return true;
}
}else{
return false;
}
}
return false;
}
}
5 钱币找零
✨ 问题描述
假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?
✨初始条件
public static void main(String[] args) {
int[][] moneyCount = { { 1, 3 }, { 2, 1 }, { 5, 4 }, { 10, 3 }, { 20, 0 } ,{50, 1}, { 100, 10 } };
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要支付的钱");
int count = maxNum(scanner.nextInt(), moneyCount);//通过maxNum方法计算出最小张数量
if (count == -1){
System.out.println("No");
}else {
System.out.println("最小张数" + count);
}
✨贪心思想
我们要想求最少张数,应该从先面值大的钱开始计算,然后再计算面值小的钱
✨题目代码
private static int maxNum(int money, int[][] moneyCount) {
int count = 0;//记录当前花费钞票的张数
for (int i = moneyCount.length - 1; i >= 0; i--) {
int num = Math.min(money / moneyCount[i][0],moneyCount[i][1]);
count += num;
money -= num * moneyCount[i][0];
}
if (money == 0){
return count;
}
return -1;
}
6 多机调度问题
✨ 问题描述
某工厂有n个独立的作业,由m台相同的机器进行加工处理。作业i所需的加工时间为ti,任何作业在被处理时不能中断,也不能进行拆分处理。现厂长请你给他写一个程序:算出n个作业由m台机器加工处理的最短时间
输入
第一行T(1<T<100)表示有T组测试数据。每组测试数据的第一行分别是整数n,m(1<=n<=10000,1<=m<=100),接下来的一行是n个整数ti(1<=t<=100)。
输出
✨案例
n =6 m =3 ti :13 15 16 2 5 20
输出结果:28
✨ 问题分析
我们针对这个问题没有最有最优解,但是可以用贪心算法来求次优解。我们再做这个问题时候,如果m >= n的话,我们的机器就一次性的把任务都并发处理,我们直接找这几个作业的最大值就行了。 如果m < n 时,我们不能随便把数据放到机器上,我们随机放的话可能会造成一个机器时间特别长,一个机器时间特别短不能够充分利用,如果我们先处理时间短的,会发生时间短的任务处理完了,剩下的时间长的,所以我们的任务就是优先处理时间长的,机器上哪个先处理完,我们们就把没有处理的数据时间长的放进去,如此往复处理完所有的作业,哪个机器最后处理完就是我们所需要的最短时间。
贪心思想:每次取剩余作业时间最长的,分配给最先结束的机器。
画图分析:
✨题目代码
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[] taskTime = new int[n];
for (int i = 0; i < n; i++) {
taskTime[i] = scanner.nextInt();
}
int num = greedStrategy(m,taskTime);
System.out.println("处理的最短时间" + num);
}
private static int greedStrategy(int m , int[] array) {
Arrays.sort(array);//进行升序排序
int n = array.length;
if(n <= m){//作业数小于等于机器数
return array[n-1];
}
//作业数大于机器数
int[] machines = new int[m];
for (int i = array.length - 1; i >= 0; i--) {
//寻找machines中最下值的下标,也就是分配任务最先结束的机器
int min = 0;
for (int j = 0; j < m; j++) {
if (machines[min] > machines[j]) {
min = j;
}
}
//最先处理完的机器 + 一个任务
machines[min] += array[i];
}
return findMax(machines);
}
private static int findMax(int[] machines) {
int max = machines[0];
for (int i = 1; i < machines.length; i++) {
if (max < machines[i]){
max = machines[i];
}
}
return max;
}
7.活动选择
✨ 问题描述:有n个需要在同一天使用同一个教室的活动a1, a2, …, an,教室同一时刻只能由一个活动使用。每个活动a[i]都有一个开始时间s[i]和结束时间f[i]。一旦被选择后,活动a[i]就占据半开时间区间[s[i],f[i])。如果[s[i],f[i])和[s[j],f[j])互不重叠,a[i]和a[j]两个活动就可以被安排在这一天。求使得尽量多的活动能不冲突的举行的最大数量。
✨案例:
int[][] events = {{2,5},{3,4},{1,6},{5,8},{5,7},{3,9},{7,10}};
输出结果:3
✨贪心思想:
我们经过了上面的几个题目,有按着时间短的先安排的:
按着上面的样例来说,选择了5 – 8 点,剩下的两个时间段就不能再选了。剩下两个时间段可以同时选择。所以说时间最短这个思路是不能够实时的。
我们安排对开始时间最短进行优先选择的话:
像上图这种情况就是选择开始时间早来进行优先选择的,很明显不可以这样安排。
我们可以用结束时间进行排序:
- 按着结束时间早的开始进行升序排序(结束时间早也就间接说明了活动举办时间相对较少)。为没有举行的活动留下更多的时间。
- 初始:在结束时间最早的活动如果有相同的结束时间,在活动任找一个活动举行,作为第一个活动
- 在剩下的一些活动中,找开始时间大于前者的结束时间的一个活动作为下一个活动。
这样能够保证我们每天安排的活动数量是最多的。
- 这样我们就可以把活动进行一个以结束时间为升序,如果结束时间相同则开始时间为升序进行排序。然后进行查找当天能够最多举行的活动的数目
画图解析:
✨题目代码
public static void main(String[] args) {
int[][] events = {{2,5},{3,4},{1,6},{5,8},{5,7},{3,9},{7,10}};
int max = getMax(events);
System.out.println(max);
}
private static int getMax(int[][] events) {
//进行以结束时间为主,开始时间为辅的升序排序。
Arrays.sort(events,(int[] arr1,int[] arr2)-> { return arr1[1] - arr2[1] == 0 ? arr1[0] - arr2[0]:arr1[1] - arr2[1];});
int count = 1;
int cur = 0;
for (int i = 1; i < events.length; i++) {
if (events[i][0] >= events[cur][1]){
count++;
cur = i;
}
}
return count;
}
8. 最多可以参加的会议数目
原题链接
✨ 问题描述:
给你一个数组 events,其中 events[i] = [startDayi, endDayi] ,表示会议 i 开始于 startDayi ,结束于 endDayi 。你可以在满足 startDayi <= d <= endDayi 中的任意一天 d 参加会议 i 。注意,一天只能参加一个会议。
请你返回你可以参加的 最大 会议数目。
✨案例:
案例1:
输入:events = [[1,2],[2,3],[3,4]]
输出:3
解释:你可以参加所有的三个会议。
安排会议的一种方案如上图。
第 1 天参加第一个会议。
第 2 天参加第二个会议。
第 3 天参加第三个会议。
案例2:
输入:events = [[1,4],[4,4],[2,2],[3,4],[1,1]]
输出:4
✨题目分析:
这个问题和第七题的思想有一定的差别,第七题是用结束时间升序进行安排活动举行。但是这个问题就不能用结束时间升序来进行解决了的。
就比如案例2来说。
所以我们不能够对结束时间进行升序排序,但是我们对开始时间进行升序排序的话,这个问题就能解决了。
对于上面的结束时间早的先执行,我们可以用优先级队列存储结束时间。把开始时间大于等于想要安排会议当天,我们就可以把他们的结束时间存储到优先级队列中。再进行计算过程中,如果优先级队列中的队头元素小于当前天,这些是结束时间小于当前天了,也就是说不能够执行的会议,我们就把他的结束时间从优先级队列中去除掉
✨题目代码
public int maxEvents(int[][] events) {
//以开始时间为升序进行排序
Arrays.sort(events,(int[] arr1,int[] arr2)-> {return arr1[0] - arr2[0];});
PriorityQueue<Integer> pq = new PriorityQueue<>();//存放结束时间的优先级队列
int count = 0;//记录可以安排的活动数量
int i = 0;//遍历活动数组的下标
int day = 0;//判断每一天是否可以参加会议,是第几天。
while (!pq.isEmpty() || i < events.length){
while (i < events.length && day == events[i][0]){//对相同起始时间的会议加入到优先级队列中
pq.offer(events[i][1]);
i++;
}
while (!pq.isEmpty() && day > pq.peek()){//删除结束时间小于当前天数的活动,也就是说删除不能够举行的会议
pq.poll();
}
if (!pq.isEmpty() && day <= pq.peek()){//对结束时间最小的会议进行分配时间
pq.poll();
count++;
}
day++;
}
return count;
}
9. 无重叠区间
原题链接
✨ 问题描述:
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
✨案例:
案例1
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
案例2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
案例3:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
✨题目分析:
该问题其实有两种思路:
第一种是可以借助题目7的思想,题目7是活动的选择,活动的选择是找无重叠的最大活动数。我们只需要求出无重叠的最大数量,然后用总小区间的个数 - 无重叠区间的最大个数,就可以求出来移除区间的最小值。
第二种思路就是直接求出需要移除的区间的额个数。我们可以按着起始时间进行升序排序,然后对重叠部分进行取舍。区间重叠无非就以下两种情况:
我们升序排序后,我们遍历数组进行判断操作。我们只需要对最新区间的结束位置进行记录,发生重叠,就把结束位置更新为暂时并不需要被移除的最后一个的结束位置。
✨题目代码
方案1:
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals,(int[] arr1,int[] arr2)-> arr1[1] - arr2[1] );//对区间结束位置进行升序排序
int count = 1;//记录不需要被移除的区间数量。
int end = intervals[0][1];//结束时间早的区间结束位置
for (int i = 1; i < intervals.length; i++) {
if (end <= intervals[i][0]){
count++;
end = intervals[i][1];
}
}
return intervals.length - count;
}
方案2:
public int eraseOverlapIntervals(int[][] intervals) {
int count = 0;//移除区间的个数
Arrays.sort(intervals,(int[] arr1,int[] arr2)-> arr1[0] - arr2[0]);//把区间数组按起始位置进行升序排列。
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][1] < end) {//区间重叠情况1:删除前者。
end = intervals[i][1];
count++;
} else if (intervals[i][0] < end){//重区间叠情况2,删除后者。
count++;
}else{//不重叠情况
end = intervals[i][1];
}
}
return count;
}