写点什么

贪心算法

用户头像
en
关注
发布于: 2021 年 01 月 30 日
贪心算法

决定整理一下算法相关的东西,简单的就不弄了,先从贪心算法开始吧,会结合例子,希望最起码自己能够讲明白这些东西。(概念源自百度百科,主要是结合例子,解释概念,提前坦白,以及所有代码都是 go 语言书写,自己实现的)

概念

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是某种意义上的局部最优解。

算法思路

1.建立数学模型描述问题

2.把求解的问题分成若干个子问题。

3.对每个子问题求解,得最优解。

4.把子问题的解局部最优解合成原来解问题的一个解。

算法例子

例子一:分发饼干问题

https://leetcode-cn.com/problems/assign-cookies/description/

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

解题思路如下:

1.要尽可能多的让孩子得到满足,就是要用最小代价满足孩子的胃口值

2.若是每个饼干都用于满足恰好胃口值的孩子,就能得到最大值

3.每个孩子从小到大的胃口值,被从小到大的饼干满足,这样用的结果就是我们能得到的最大满足孩子个数

func findContentChildren(g []int, s []int) int {	sort.Ints(g)	sort.Ints(s)	var number int //能满足学生总数	var sindex int //已被满足学生坐标	for i := 0; i < len(s); i++ {		if sindex < len(g) && s[i] >= g[sindex] {			number++			sindex++		}	}	return number}
复制代码


例子二:无重叠区间

https://leetcode-cn.com/problems/non-overlapping-intervals/description/?utmsource=LCUS&utmmedium=ipredirect&utmcampaign=transfer2china

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。

区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。

解题思路如下:

1.要无重叠区间,若右闭合一致,区间间隔更长的比区间间隔更短的更容易有重叠,本着贪心思想遇到这种情况直接选择左闭合更大的保留(重复的直接剔除)

2.按照 1 情况处理一遍以后,从小到大的筛选重叠区间就是最优解

func eraseOverlapIntervals(intervals [][]int) int {	if len(intervals)==0{		return 0	}	var number int //需要剔除的区间数目	qmap := make(map[int][]int)//key为区间结尾	rightv:=make([]int,0) //记录右闭合值	for i:=0;i<len(intervals);i++{		if qmap[intervals[i][1]] == nil{			//这个结尾还没有存在			qmap[intervals[i][1]] = intervals[i]			rightv = append(rightv,intervals[i][1])		}else{			//结尾已经存在,如果左闭合更大,说明要替换			if qmap[intervals[i][1]][0] < intervals[i][0]{				qmap[intervals[i][1]] = intervals[i]			}			number++		}	}	if len(rightv)==1{		return number	}	sort.Ints(rightv)	last:=rightv[0]	for i:=1;i<len(rightv);i++{		if qmap[rightv[i]][0] < last{			number++		}else{			last = rightv[i]		}	}	return number}
复制代码


例子三:投飞镖刺破气球

https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/description/

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

解题思路如下:

同款题目

上一题找无重叠区间,重点是上一个区间的结尾与下一个的开始,这一题,思考的是每一箭要引爆尽可能多的气球,只要每一箭引爆的气球数目最多,那么就可以用最少的弓箭数目解决。

1.去除所有同开头,但是右闭合更大的气球,因为他们一定会跟另一个一起爆炸

2.按照开头从小打大,注意每一箭能覆盖的长度范围,左闭合一定小于第一个要射爆气球的右闭合,右闭合取一起射爆气球的最小右闭合。

3.最终结果就是最少可用弓箭数目

func findMinArrowShots(points [][]int) int {	if len(points) == 0 {		return 0	}	qmap := make(map[int][]int) //key为区间结尾	leftv := make([]int, 0)     //记录左闭合值	for i := 0; i < len(points); i++ {		if qmap[points[i][0]] == nil {			//这个开头还没有存在			qmap[points[i][0]] = points[i]			leftv = append(leftv, points[i][0])		} else {			//开头已经存在,如果右闭合更大,说明一定会一起爆,			if qmap[points[i][0]][1] > points[i][1] {				qmap[points[i][0]] = points[i]			}		}	}	if len(leftv) == 1 {		return 1	}	sort.Ints(leftv)	last := qmap[leftv[0]][1]	var number int //弓箭数目	number = 1	for i := 1; i < len(leftv); i++ {		if qmap[leftv[i]][0] <= last {			//可以一起爆,但是要注意边界			if qmap[leftv[i]][1] < last {				last = qmap[leftv[i]][1]			}			continue		} else {			//上一轮的结束了			number++			last = qmap[leftv[i]][1]		}	}	return number}
复制代码


例子四:根据身高重建队列

https://leetcode-cn.com/problems/queue-reconstruction-by-height/description/

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

解题思路如下:

1.无论怎么解,只要能把单个人放到正确的位置,最后综合的结果就是正确的结论

2.我们按照身高顺序,从高往后排序,先确定高的,将低的按照顺序插入,由于 ki 是身高比他高的人的计数,所以用这个方法可以保证每次都是局部最优,整合是整体最优

func reconstructQueue(people [][]int) [][]int {	if len(people) <= 1 {		return people	}	//0 是身高	//1 是前面比他高的人数	length := len(people)	res := make([][]int, length)	high := make([]int, 0)	pmap := make(map[int][][]int)	for i := 0; i < len(people); i++ {		pmap[people[i][0]] = append(pmap[people[i][0]], people[i])		if len(pmap[people[i][0]]) == 1 {			high = append(high, people[i][0])		}
} sort.Ints(high) for i := len(high) - 1; i >= 0; i-- { //排序,同高度的要保证k小的优先排序,不然顺序就乱掉了 sort.Slice(pmap[high[i]], func(k, z int) bool { if pmap[high[i]][k][1] < pmap[high[i]][z][1] { return true } return false }) arrs := pmap[high[i]]
for j := 0; j < len(arrs); j++ { index := arrs[j][1] if res[index] == nil { res[index] = arrs[j] } else { //全部向后一位,没集齐的时候最后一位一定是空 for k := length - 1; k > index; k-- { res[k] = res[k-1] } res[index] = arrs[j] } } } return res}
复制代码


例子五:经典股票问题

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

解题思路如下:

1.从第一天,第二天开始找最优,目标是找到前面最小的股票价钱,再从后面找到与最小股票价钱相差最大的值

2.假设有 7 天,前 3 天股票获取的最大收益如果整个流程没有能超过的,那么前 3 天获得的最优解就是整个流程的最优解

func maxProfit(prices []int) int {	if len(prices) <= 1 {		return 0	}	var max int	//前几日最小的值	min := prices[0]	for i := 1; i < len(prices); i++ {		if prices[i]-min < 0 {			min = prices[i]		} else {			if prices[i]-min > max {				max = prices[i] - min			}		}	}	return max}
复制代码


例子六:非递减数列

https://leetcode-cn.com/problems/non-decreasing-array/

给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。我们是这样定义一个非递减数列的: 对于数组中所有的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。

解题思路如下:

最多改变一个元素,就说明非递减的情况只能出现一次

我们可以从这个判断条件入手,分析具体情况

1.若是在边界,开头出现 0>1,我们为了递减必然删除 0,计数加 1,结尾出现,处理掉,计数加 1,计数超过 1 则无法达成条件

2.在数组中间,有以下处理方式

1 2 3 5 4 6 7 ==》删除

3 4 2 3 ==》 由于 3>2 所以失败,3 要小于 2,也就是 i-1<i+1,若是 i-1 不小于 i+1,那么可以直接删除 i

2 3 1 5 ==》处理的不再是逆序开始的 i,而是要处理 i+1,不同原因是 i<i+2

所以可以得出结论

计数大于 1,false

在数组中间 i-1>i+1,且 i<i+2 才能造成无法处理的情况,也就是 3 4 2 3,虽然走下去只有一个 count 但是也无法处理

func checkPossibility(nums []int) bool {	length := len(nums)	counter := 0	for i := 0; i < length-1; i++ {		if nums[i] > nums[i+1] {			counter++			isOutOfBounds := i != 0 && i+2 < length			if counter > 1 || (isOutOfBounds && nums[i] > nums[i+2] && nums[i-1] > nums[i+1]) {				return false			}		}	}	return true}
复制代码


例子六:最大子序和

https://leetcode-cn.com/problems/maximum-subarray/

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

解题思路如下:

1.求最大子序和,那么任何一段的和取最大就是

2.tmp 为中转值,若是 tmp+nums[i]<0 再往下加必然不会是最大,就从 0 开始,又考虑到数组可能为全负数,此种情况就变成了 max 取数组中最大的负数,所以也要考虑

3.若是 tmp+nums[i]>0 就说明加下去还是有可能得出更大的值,拿结果与之前的 max 判断,进行替换。

func maxSubArray(nums []int) int {	max := nums[0]	var tmp int	for i:=0;i<len(nums);i++{		if tmp + nums[i] < 0{			tmp = 0			if nums[i]>max{				max = nums[i]			}		}else{			tmp = tmp + nums[i]			if tmp>max{				max = tmp			}		}	}	return max}
复制代码


例子七:分隔字符串使同种字符出现在一起

https://leetcode-cn.com/problems/partition-labels/

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

解题思路如下:

1.要尽可能多的分段,那么以最小的字母数为一段,每段区分,所有的集合就是最优解


func partitionLabels(S string) []int {	smap:=make(map[uint8]int)	send:=make(map[uint8]int)	for i:=0;i<len(S);i++{		smap[S[i]]++		send[S[i]] = i	}	var arrs []int	star:=0	for star<len(S){		number:=smap[S[star]]		end:=send[S[star]]		if number+star==end+1{			//数目和字母number一样			arrs = append(arrs,number)			star = end+1		}else{			str:=make(map[uint8]bool)			str[S[star]] = true			for i:=star;i<=end;i++{				if S[star]!=S[i]&&str[S[i]]==false{					//有新字母					str[S[i]] = true					number = number + smap[S[i]]					if send[S[i]] > end{						end = send[S[i]]					}					if end+1==number+star{						arrs = append(arrs,number)						star = end+1						break					}				}			}		}	}	return arrs}
复制代码

参考:

https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E7%9B%AE%E5%BD%95.md


用户头像

en

关注

还未添加个人签名 2018.06.14 加入

还未添加个人简介

评论

发布
暂无评论
贪心算法