贪心算法
# 贪心
# 常规贪心
# 分发饼干 (opens new window)
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子
i
,都有一个胃口值g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干j
,都有一个尺寸s[j]
。如果s[j] >= g[i]
,我们可以将这个饼干j
分配给孩子i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
- [ ] 局部最优-> 全局最优
# 摆动序列 (opens new window)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为** 摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。- 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组
nums
,返回nums
中作为 **摆动序列 **的 最长子序列的长度 。
- [x] 方法一:贪心
- [ ] 方法二:动态规划
func wiggleMaxLength(nums []int) int {
res := 1
preDiff, curDiff := 0 , 0
for i:=0; i<len(nums)-1; i++ {
curDiff = nums[i+1] - nums[i]
if (preDiff >= 0 && curDiff < 0 ) || (preDiff <= 0 && curDiff > 0) {
res++
preDiff = curDiff
}
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
# 最大子数组和 (opens new window)
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 子数组是数组中的一个连续部分。 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
- [ ] 贪心:
- [ ] 动态规划:
func maxSubArray(nums []int) int {
res := nums[0]
for i:=1; i<len(nums);i++ {
if nums[i-1] > 0 {
nums[i] += nums[i-1]
}
res = max(res, nums[i])
}
return res
}
2
3
4
5
6
7
8
9
10
# 跳跃游戏 (opens new window)
# 跳跃游戏 (opens new window)2(最小跳跃次数)
给定一个长度为
n
的 0 索引整数数组nums
。初始位置为nums[0]
。 每个元素nums[i]
表示从索引i
向前跳转的最大长度。换句话说,如果你在nums[i]
处,你可以跳转到任意nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
重点
- [ ] 贪心:O(n)、O(1)
- [ ] 动态规划:O(n*m)、O(n)
// 贪心,O(n) O(1), 由于
func jump(nums []int) int {
if len(nums) == 1 {
return 0
}
curDis, NextDis := 0, 0
res := 0
for i:=0; i<len(nums); i++ {
NextDis = max(NextDis, i+nums[i]) // 能到的最远的距离
if i == curDis { //起跳一次,
res++ //起跳一次,记录下
if NextDis >= len(nums)-1 {
break
}
curDis = NextDis
}
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 动态规划
func jump(nums []int) int {
dp := make([]int, len(nums))
dp[0] = 0
for i:=1; i<len(nums); i++ {
dp[i] = math.MaxInt32
}
for i:=0; i<len(nums); i++ {
for j:=0; j<=nums[i]; j++ {
if i+j<len(nums) {
dp[i+j] = min(dp[i+j], dp[i]+1)
}
}
}
return dp[len(nums)-1]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# K 次取反后最大化的数组和 (opens new window)
给你一个整数数组
nums
和一个整数k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
贪心规则:将最小的负数翻转,直到没有负数,再反转一个最小的负数(k%2==1)
func largestSumAfterKNegations(nums []int, k int) int {
sort.Slice(nums, func(i, j int) bool {
return math.Abs(float64(nums[i])) > math.Abs(float64(nums[j]))
})
res := 0
for i := 0; i < len(nums); i++ {
if k > 0 && nums[i] < 0 {
nums[i] = -nums[i]
k--
}
}
if k%2 == 1 {
nums[len(nums)-1] = -nums[len(nums)-1]
}
for i := 0; i < len(nums); i++ {
res += nums[i]
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 加油站 (opens new window)
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
_ 升。 你有一辆油箱容量无限的的汽车,从第i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
_升。你从其中的一个加油站出发,开始时油箱为空。 给定两个整数数组gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则 保证 它是 唯一 的。
- [ ] 贪心1
- [ ] 贪心2
func canCompleteCircuit(gas []int, cost []int) int {
rest, minGas := 0, math.MaxInt32
for i:=0; i<len(cost); i++ {
rest += (gas[i] - cost[i])
if rest < minGas {
minGas = rest
}
}
if rest < 0 {
return -1
}
if minGas >= 0 {
return 0
}
for i:=len(cost)-1; i>=0; i-- {
minGas += (gas[i] - cost[i])
if minGas >= 0 { //大于0表示该次加的油能够覆盖最缺油的路程
return i
}
}
return -1
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 方法2,也是贪心算法
func canCompleteCircuit(gas []int, cost []int) int {
start, curSum, totalSum := 0, 0, 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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 分发糖果 (opens new window)
n
个孩子站成一排。给你一个整数数组ratings
表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。- 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
左右两次扫描
func candy(ratings []int) int {
need := make([]int, len(ratings))
for i := 0; i < len(ratings); i++ {
need[i] = 1
}
for i := 1; i < len(ratings); i++ {
if ratings[i] > ratings[i-1] {
need[i] = need[i-1] + 1
}
}
for i := len(ratings) - 1; i >= 1; i-- {
if ratings[i-1] > ratings[i] {
need[i-1] = max(need[i-1], need[i]+1)
}
}
res := 0
for i := 0; i < len(need); i++ {
res += need[i]
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 根据身高重建队列 (opens new window)
假设有打乱顺序的一群人站成一个队列,数组
people
表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki]
表示第i
个人的身高为hi
,前面 正好 有ki
个身高大于或等于hi
的人。 请你重新构造并返回输入数组people
所表示的队列。返回的队列应该格式化为数组queue
,其中queue[j] = [hj, kj]
是队列中第j
个人的属性(queue[0]
是排在队列前面的人)。
- [ ] 贪心规则:等于或大于自己身高的会被统计到 k , 这是一个规则,那就先按照这个排好序;再根据 k 大小来调整顺序。多个规则时一个一个来
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, p := range people {
copy(people[p[1]+1: i+1], people[p[1]:i+1]) //有点巧妙,将p[1]位置之后的数向后移动一位,空出p[1]的位置,来放置p
people[p[1]] = p
}
return people
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 区间问题
# 用最少数量的箭引爆气球 (opens new window)
- [ ] 未解决
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组
points
,其中points[i] = [xstart, xend]
表示水平直径在xstart
和xend
之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标x
处射出一支箭,若有一个气球的直径的开始和结束坐标为x``start
,x``end
, 且满足xstart ≤ x ≤ x``end
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。 给你一个数组points
,_返回引爆所有气球所必须射出的 最小 弓箭数 _。 输入:points = [[10,16],[2,8],[1,6],[7,12]] 输出:2 解释:气球可以用2支箭来爆破: -在x = 6处射出箭,击破气球[2,8]和[1,6]。 -在x = 11处发射箭,击破气球[10,16]和[7,12]。
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][1], points[i][1]); // 更新重叠气球最小右边界,覆盖该位置的值,留到下一步使用
}
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 无重叠区间 (opens new window)
给定一个区间的集合
intervals
,其中intervals[i] = [starti, endi]
。返回 _需要移除区间的最小数量,使剩余区间互不重叠 _。
本质在求最大不重叠区间数
func eraseOverlapIntervals(intervals [][]int) int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][1] < intervals[j][1]
})
res := 1
for i:=1; i<len(intervals); i++ {
if intervals[i-1][1] <= intervals[i][0] {
res++
} else {
intervals[i][1] = min(intervals[i][1], intervals[i-1][1])
}
}
return len(intervals) - res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 划分字母区间 (opens new window)
给你一个字符串
s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。 注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s
。 返回一个表示每个字符串片段的长度的列表。
- [ ] 贪心1:使用map记录每个元素最后出现的位置,第二次遍历str, 若s[i] == end,则划分一段 。
- [ ] 贪心2:使用每个字符的出现的区间,排序,并且合并有重叠的区间。即找到所有的非重叠区间
该方法需要额外空间来存储线段,且还要排序,空间消耗也较大
// 贪心1
func partitionLabels(s string) []int {
m := map[byte]int{}
for i:=0; i<len(s); i++ {
m[s[i]] = i
}
res := []int{}
num := 0
end_p := m[s[0]]
for i:=0; i<len(s); i++ {
num += 1
if m[s[i]] > end_p {
end_p = m[s[i]]
}
if i == end_p {
res = append(res, num)
num = 0
}
}
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func partitionLabels(s string) []int {
m := make(map[byte][2]int, 0)
for i := 0; i < len(s); i++ {
if v, ok := m[s[i]]; !ok {
m[s[i]] = [2]int{i, i}
} else {
m[s[i]] = [2]int{v[0], i}
}
}
vec := make([][2]int, 0)
for _, v := range m {
vec = append(vec, [2]int{v[0], v[1]})
}
res := []int{}
sort.Slice(vec, func(i, j int) bool {
if vec[i][0] == vec[j][0] {
return vec[i][1] < vec[j][1]
}
return vec[i][0] < vec[j][0]
})
start := 0
for i := 1; i < len(vec); i++ {
if vec[i-1][1] < vec[i][0] {
res = append(res, vec[i-1][1]-start+1)
start = vec[i][0]
} else {
vec[i] = [2]int{vec[i][0], max(vec[i][1], vec[i-1][1])}
}
}
res = append(res, vec[len(vec)-1][1] - start + 1)
return res
}
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
32
33
34
35
# 合并区间 (opens new window)
以数组
intervals
表示若干个区间的集合,其中单个区间为intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
纯区间合并问题,较为简单
func merge(intervals [][]int) [][]int {
sort.Slice(intervals, func(i, j int)bool {
return intervals[i][0] < intervals[j][0]
})
res := [][]int{}
start := intervals[0][0]
for i:=1; i<len(intervals); i++ {
if intervals[i-1][1] < intervals[i][0] {
res = append(res, []int{start, intervals[i-1][1]})
start = intervals[i][0]
} else {
intervals[i] = []int{intervals[i][0], max(intervals[i-1][1], intervals[i][1])}
}
}
res = append(res, []int{start, intervals[len(intervals)-1][1]})
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 单调递增的数字 (opens new window)
当且仅当每个相邻位数上的数字
x
和y
满足x <= y
时,我们称这个整数是单调递增的。 给定一个整数n
,返回 小于或等于_n_
的最大数字,且数字呈 单调递增 。
字符串转换:
strconv.Itoa(n)
、strconv.Atoi string不可修改,转化为切片:[]byte()
func monotoneIncreasingDigits(n int) int {
s := strconv.Itoa(n)
ss := []byte(s)
if len(ss) <= 1 {
return n
}
for i := len(ss) - 1; i > 0; i-- {
if ss[i-1] > ss[i] {
ss[i-1] -= 1
for j := i; j < len(ss); j++ { //后面的全部置为9
ss[j] = '9'
}
}
}
res, _ := strconv.Atoi(string(ss))
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 监控二叉树 (opens new window)
给定一个二叉树,我们在树的节点上安装摄像头。 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 计算监控树的所有节点所需的最小摄像头数量。
func minCameraCover(root *TreeNode) int {
res := 0
var dfs func(node *TreeNode) int
dfs = func(node *TreeNode) int {
if node == nil {
return 2
}
left := dfs(node.Left)
right := dfs(node.Right)
// # 情况1: 两字节点无需被考虑,则当前节点选择不装,并告知上层需要考虑我
if left == 2 && right == 2 {
return 0
}
// # 情况2: 子节点需要被影响,此时必须装
if left == 0 || right == 0 {
res++
return 1
}
// 子节点已安装,上层节点无需考虑我
// # left == 1 && right == 2 左节点有摄像头,右节点有覆盖
// # left == 2 && right == 1 左节点有覆盖,右节点有摄像头
// # left == 1 && right == 1 左右节点都有摄像头
return 2
}
if dfs(root) == 0 {
res++
}
return res
}
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
32
33
34