贪心算法


理论基础

局部最优推出全局最优,并且想不出反例

想不出反例就是随便想一下好了,不用严格证明

两个极端

  1. 贪心算法题目太简单(基于常识)

  2. 贪心算法题目太难(忽略尝试的地方)

套路: 无套路 策略: 举反例

贪心就是做过了所以知道了(看教课书上讲解贪心可以是一堆公式,估计大家连看都不想看,所以数学证明就不在我要讲解的范围内了,所以贪心不需要证明)

面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。

总结:

贪心算法就是刷,没有套路,局部最优

分发饼干

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

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

思路:没啥想说的

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func findContentChildren(g []int, s []int) int {
sort.Ints(g)
sort.Ints(s)
var count int
for _, ch := range g {
for i, pin := range s {
if ch <= pin {
count++
s = s[i+1:]
break
}
}
}
return count
}

摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

示例 1:

1
2
3
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

思路:

woc,看完代码就知道思路了,而且感觉这个思路跟贪心没有关系啊

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func wiggleMaxLength(nums []int) int {
if len(nums) <= 2 {
return len(nums)
}
up, down := 1, 1
for i := 1; i < len(nums); i++ {
if nums[i] > nums[i-1] {
up = down + 1
} else if nums[i] < nums[i-1] {
down = up + 1
}
}

return max(up, down)
}

最大子树组和

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

子数组是数组中的一个连续部分。

示例 1:

1
2
3
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 贪心算法
func maxSubArray(nums []int) int {
maxSum := nums[0]
sum := 0
for i := 0; i < len(nums); i++ {
sum = sum + nums[i]
if sum > maxSum {
maxSum = sum
}
if sum < 0 {
sum = 0
}
}
return maxSum
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 动态规划
func maxSubArray(nums []int)int{
xam := nums[0]
for i:=1 ; i<len(nums);i++{
if nums[i-1] > 0 {
nums[i] += nums[i-1]
}
if nums[i] > xam {
xam = nums[i]
}
}
return xam
}

买股票的最佳时机||

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

1
2
3
4
5
输入: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 。

思路

代码随想录的讲法

如果想到其实最终利润是可以分解的,那么本题就很容易了!

如何分解呢?

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!

那么根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1])…..(prices[1] - prices[0])。

但是我感觉上面的思路是有问题的,因为第一天是不知道第二天的状况的,那么为什么他能自然而然的收集正利润,有可能第一天买的高,第二天卖的低啊

这道题的正规解法是动态规划,等到了动态规划再来做这道题拜拜

1
2
3
4
5
6
7
8
9
func maxProfit(prices []int)int{
var sum int
for i:= 1;i<len(prices);i++{
if prices[i] > prices[i-1]{
sum += prices[i] - prices[i-1]
}
}
return sum
}

跳跃游戏

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

示例 1:

1
2
3
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

思路:

我觉得代码随想录的思路非常好,就直接用他的思路

刚看到本题一开始可能想:当前位置元素如果是 3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?

其实跳几步无所谓,关键在于可跳的覆盖范围!

不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。

这个范围内,别管是怎么跳的,反正一定可以跳过来。

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!

每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。

贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func canJump(nums []int)bool{
cover := 0
cover = nums[0]
for i:=0 ; i<=cover ; i++{
if i + nums[i] > cover{
cover = i + nums[i]
}

if cover >= len(nums) - 1{
return true
}
}
return false
}

小总结

我发现贪心算法更多算的是两个数之间的差值来算,而不是盯着某几个数的点来看

跳跃游戏||

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

示例 1:

1
2
3
4
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

思路:

这道题跟上面的题目唯一的区别就是跳跃范围改变了

但是思路还是求覆盖面

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func jump(nums []int) int {
lastDistance := 0
curDistance := 0
minStep := 0

for i := 0; i < len(nums); i++ {
if i == lastDistance+1 {
minStep++
lastDistance = curDistance
}
curDistance = max(curDistance, nums[i]+i)
}
return minStep
}

K次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]

重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

我只能做做简单题了,唉…………………….

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func largestSumAfterKNegations(nums []int, k int) int {
sort.Ints(nums)
for i := 0; i < len(nums) && k > 0; i++ {
if nums[i] < 0 {
nums[i] = -nums[i]
k--
}
}
if k%2 == 1 {
sort.Ints(nums)
nums[0] = -nums[0]
}

var sum int
for _, v := range nums {
sum += v
}

return sum
}

加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

1
2
3
4
5
6
7
8
9
10
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

思路:

这道题其实限定条件很多哇,才会可以用贪心做

  1. 唯一解
  2. 线性行驶
  3. 只要每一站的 原有的油+加上的油>支出的油即可

情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func canCompleteCircuit(gas []int, cost []int) int {
curSum := 0
totalSum := 0
start := 0 // 记录起始位置

for i := 0; i < len(gas); i++ {
curSum += gas[i] - cost[i]
totalSum += gas[i] - cost[i]
if curSum < 0 {
start = i + 1
curSum = 0
}
}

if totalSum < 0 {
return -1
}

return start
}

分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

示例 1:

1
2
3
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

这道题好难,不会…….

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func candy(ratings []int) int {
need := make([]int, len(ratings))
length := len(ratings)
for i := 0; i < length; i++ {
need[i] = 1
}

for i := 1; i < length; i++ {
if ratings[i] > ratings[i-1] {
need[i] = need[i-1] + 1
}
}

for i := length - 1; i > 0; i-- {
if ratings[i] < ratings[i-1] && need[i] >= need[i-1] {
need[i-1] = max(need[i-1], need[i]+1)
}
}

sum := 0
for _, v := range need {
sum += v
}

return sum
}

柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

示例 1:

1
2
3
4
5
6
7
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func lemonadeChange(bills []int) bool {
num5 := 0
num10 := 0

for i := 0; i < len(bills); i++ {
switch bills[i] {
case 5:
num5++
case 10:
if num5 == 0 {
return false
}
num5--
num10++
case 20:
need5 := 1
if num10 == 0 {
need5 = 3
} else {
num10--
}
if num5 < need5 {
return false
} else {
num5 -= need5
}
}
}

return true
}

根据身高重建队列

这道题我不会反正,好难。

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

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

思路:

确定一遍然后贪心另一边,两边一起考虑,就会顾此失彼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func reconstructQueue(people [][]int) [][]int {
sort.Slice(people, func(i, j int) bool {
if people[i][0] == people[j][0] {
return people[i][1] < people[j][1]
}
return people[i][0] > people[j][0]
})

for i, v := range people {
copy(people[v[1]+1:i+1], people[v[1]:i+1])
people[v[1]] = v
}

return people
}

用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

思路:

保证每个范围中都能射中,加起来就是最大范围,注意这个范围是会变化的

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func findMinArrowShots(points [][]int)int{
var res int = 1
sort.Slice(points, func(i, j int) bool {
return points[i][0] < points[j][0]
})

for i:=1 ; i<len(points) ;i++{
if points[i-1][1] < points[i][0]{
res++
} else {
points[i][1] = min(points[i][1], points[i-1][1])
}
}

return res
}

无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

跟上面的一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func eraseOverlapIntervals(intervals [][]int) int {
if len(intervals) == 0 {
return 0
}

sort.Slice(intervals, func(i, j int) bool {
return intervals[i][1] < intervals[j][1]
})

count := 0
end := intervals[0][1]
for i := 1; i < len(intervals); i++ {
if intervals[i][0] >= end {
end = intervals[i][1]
} else {
count++
end = min(end, intervals[i][1])
}
}
return count
}

合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

思路

这道题就是前面几道题的思路,但是比前面几道题要简单的多

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func merge(intervals [][]int) [][]int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
res := make([][]int, 0)
res = append(res, intervals[0])
cur := intervals[0][1]
for i := 1; i < len(intervals); i++ {
if intervals[i][0] <= cur {
cur = max(cur, intervals[i][1])
res[len(res)-1][1] = cur
} else {
cur = intervals[i][1]
res = append(res, intervals[i])
}
}
return res
}

划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

思路

找出区间内的最大范围就行

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func partitionLabels(s string) []int {
last := [26]int{}
for i,v := range s {
last[v-'a'] = i
}

start , end := 0 , 0
var res []int
for i , v := range s {
if last[v-'a'] > end{
end = last[v-'a']
}
if end == i {
res = append(res,end-start+1)
start = end+1
}
}
return res
}

单调递增的数字

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

思路

比如说 1243这个数要递增的最大–> 1239 其中就经历了 第三位数减一加上第四位数变九,依次循环即可

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func monotoneIncreasingDigits(N int) int {
if N < 10 {
return N
}
s := []byte(strconv.Itoa(N))
for i := 1; i < len(s); i++ {
if s[i] < s[i-1] {
for j := i; j < len(s); j++ {
s[j] = '9'
}
s[i-1]--
i = 0
}
}
res, _ := strconv.Atoi(string(s))
return res
}

额外题目:监控二叉树

有点难……

总结

其实我也不知道我的贪心写了个啥,就感觉贪心算法没啥好注意的,以后碰到了估计还是蒙蔽

但是还是记住贪心永远是局部最优推出全局最优