第 3 章 常用算法
3.6 贪心算法
3.6.1 概述
贪心算法在解决问题时,采取的是逐步选择当前状态下最优的选项(即局部最优解),并期望通过这些局部最优解得到全局最优解。在每一步中,贪心算法都会选择当前看起来最优的选择,不考虑未来的选择。并通过一系列局部最优的选择,最终达到全局最优解(虽然并非总是能得到最优解,贪心算法的有效性依赖于问题的特性)。
贪心算法的特点:
-
选择性:在每一步选择中,贪心算法根据某种启发式策略选择局部最优解。
-
不可回溯:一旦做出了选择,就不能回退或重新考虑。
-
局部最优:每一步的选择都依赖于局部最优,但并不保证整个问题能得到全局最优解。
贪心算法能够得到全局最优解的条件是问题需要满足贪心选择性质和最优子结构:
-
贪心选择性质:通过局部最优的选择可以导出全局最优解。也就是说,每一步的局部选择都不会影响后续的选择,最终可以得到全局最优。
-
最优子结构:问题的最优解包含子问题的最优解,即可以通过解决子问题来构建问题的最终解。
3.6.2 案例一:最大交换
力扣670题https://leetcode.cn/problems/maximum-swap/description/
现有一个非负整数,至多可以交换一次数字中的任意两位。返回能得到的最大值。
示例:
-
输入:2736
-
输出:7236
解释:交换数字2和数字7。
1)思路分析
从右向左遍历,同时维护一个最大数的索引。
-
如果当前位置的数大于最大数,则更新最大数索引。
-
否则将当前位置的数与最大数进行交换,交换后的数更新到result中,再将两个位置的数交换回来将数组恢复原样。
遍历结束后,result中的数就是能得到的最大值。
Python
def maximumSwap(num):
result = num
num_list = list(str(num))
max_index = -1 # 最大值的索引
for i in range(len(num_list) - 1, -1, -1):
# 当前值大于最大值时,更新最大值的索引
if num_list[i] > num_list[max_index]:
max_index = i
# 当前值小于最大值时,交换,更新result,再交换回来
else:
num_list[i], num_list[max_index] = num_list[max_index], num_list[i]
result = max(result, int("".join(num_list)))
num_list[i], num_list[max_index] = num_list[max_index], num_list[i]
return result
3.6.3 案例二:分发糖果
力扣135题https://leetcode.cn/problems/candy/description/
n个孩子站成一排。用一个整数数组ratings表示每个孩子的评分。按照以下要求,给这些孩子分发糖果:
-
每个孩子至少分配到1个糖果。
-
相邻两个孩子评分更高的孩子会获得更多的糖果。
给每个孩子分发糖果,计算并返回需要准备的最少糖果数目。
1)方法一
每个孩子先分发1个糖果。
从左向右遍历,如果右边孩子比左边孩子评分高,则右边孩子糖果数量应该>左边孩子糖果数量,这时令右边孩子糖果数量为左边孩子糖果数量+1。
再从右向左遍历,如果左边孩子比右边孩子评分高,则左边孩子糖果数量应该为max(右边孩子糖果数量+1, 自己糖果数量)。
Python
def candy(ratings):
n = len(ratings)
# 每个孩子先分发1个糖果
candy_num = [1] * len(ratings)
# 从左向右遍历,如果右边评分更高,则右边孩子的糖果数改为左边孩子的糖果数+1
for pos in range(n - 1):
if ratings[pos] < ratings[pos + 1]:
candy_num[pos + 1] = candy_num[pos] + 1
# 从右向左遍历,如果左边评分更高,则左边孩子的糖果数改为右边孩子的糖果数+1
total_candies = candy_num[-1]
for pos in range(n - 2, -1, -1):
if ratings[pos] > ratings[pos + 1]:
if candy_num[pos] <= candy_num[pos + 1]:
candy_num[pos] = candy_num[pos + 1] + 1
total_candies += candy_num[pos]
return total_candies
2)方法二
每个孩子先分发1个糖果。
按孩子评分相较于前一个孩子评分的增加、减少、不变分成3个区域,分别是上升区、下降区、平缓区。

在进入不同区域时,执行不同的操作:
- 上升区
上升区长度+1,并清空之前的下降区长度。同时在result中累加糖果数。上升区或下降区长度每增加1,result中都需要增加与上升区或下降区长度相同数量的糖果。

- 下降区
下降区存在一个特殊的准下降区。当从上升区进入下降区后,第一个进入的为准下降区。
准下降区不计入下降区长度。
当下降区长度等于上升区长度时,准下降区转变为真下降区并计入下降区长度。
下降区内清空之前的上升区长度,同时在result中累加糖果数。


- 平缓区
清空所有记录,没有其他操作。
Python
def candy(ratings):
# 每个孩子分发一个糖果
result = len(ratings)
# 上升区长度
up_length = 0
# 上升区长度的记录
up_num = 0
# 下降区长度
down_length = 0
for i in range(1, len(ratings)):
# 如果处于上升区
if ratings[i] > ratings[i - 1]:
# 上升区长度+1,清空下降区长度,并在result中累加糖果数
up_length += 1
up_num = up_length
down_length = 0
result += up_length
# 如果处于下降区
elif ratings[i] < ratings[i - 1]:
# 如果上升区长度为0,说明前一个位置已经不在上升区内,下降区长度+1
if up_length == 0:
down_length += 1
# 如果下降区长度已经等于之前上升区长度,说明峰值的糖果数由下降区决定,下降区长度+1
if down_length == up_num:
down_length += 1
# 清空上升区长度
up_length = 0
result += down_length
# 如果处于平缓区
else:
# 清空所有记录
up_length = 0
down_length = 0
up_num = 0
return result