贪心
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择策略,以便产生全局最优解的算法思想。这种局部最优的选择通常基于某种特定的度量标准,它试图通过一系列最佳选择来构建最终的解决方案。
贪心算法的特点:
- 局部最优选择:在问题求解的每一步中,总是做出在当前看来最好的选择。
- 无回溯:所做的选择一旦确定,就不会改变,即算法不会回头去修正之前的选择。
- 简单高效:通常比其他算法(如动态规划或分治法)更容易理解和实现。
- 不总是得到全局最优解:贪心算法并不总是能得到全局最优解,只有当问题具有贪心选择性质和最优子结构性质时才适用。
贪心算法的应用场景:
- 活动选择问题:选择尽可能多的不冲突的活动。
- 霍夫曼编码:用于数据压缩,构建最优前缀码。
- 最小生成树:如普里姆算法(Prim’s algorithm)和克鲁斯卡尔算法(Kruskal’s algorithm)。
- Dijkstra算法:寻找图中单源最短路径。
- 分数背包问题:允许物品分割的问题。
贪心算法的设计步骤:
- 证明最优子结构:证明问题可以分解为子问题,并且子问题的解也是最优的。
- 定义一个选择性质:定义一种策略,使得每次选择都是局部最优的。
- 证明贪心选择:证明局部最优选择能够导致全局最优解。
- 构造贪心算法:基于贪心选择性质设计算法。
- 证明正确性:通过数学归纳法或其他方式证明算法的正确性。
实现注意事项:
- 选择标准:需要有一个明确的选择标准来决定哪种选择是最好的。
- 可行性检查:每一步都要确保所作的选择是可行的,即符合问题的约束条件。
- 贪心选择的顺序:某些情况下,选择的顺序可能会影响最终的结果。
贪心算法的有效性取决于问题本身的特性。对于某些问题,贪心算法可以快速找到最优解;而对于另一些问题,则可能无法得到最优解。因此,在实际应用中需要对问题进行仔细分析以确定贪心算法是否适用。
当然可以。让我们来看一个简单的贪心算法的例子——硬币找零问题。在这个问题中,我们需要用最少数量的硬币来给出找零。假设我们有无限数量的几种面值的硬币(例如1美分、5美分、10美分和25美分)。我们的目标是最小化找零所需的硬币数量。
例题: leetcode.322.零钱兑换
https://leetcode.cn/problems/coin-change/description/
这道题用贪心只能通过51 / 189
因为贪心只寻找局部最优,
要想全部通过,要使用动态规划
def make_change(coins, amount):
"""
使用贪心算法计算最少硬币数。
参数:
coins (list of int): 可供使用的硬币面额列表。
amount (int): 需要找零的金额。
返回:
int: 最少需要的硬币数量。
"""
# 对硬币面额进行降序排序
coins.sort(reverse=True)
# 初始化结果变量
coin_count = 0
# 遍历硬币列表
for coin in coins:
# 计算当前面额硬币的最大数量
while amount >= coin:
amount -= coin
coin_count += 1
return coin_count
# 定义可用的硬币面额
coins = [25, 10, 5, 1]
# 定义需要找零的金额
amount = 63
# 调用函数并打印结果
print("最少需要的硬币数量:", make_change(coins, amount))
这段代码首先定义了一个 make_change
函数,该函数接收硬币面额列表和需要找零的金额作为输入。它先将硬币面额按降序排列,然后从最大面额开始尝试使用尽可能多的该面额硬币,直到剩余的金额不足以使用更大的面额为止。最后,函数返回所需的最少硬币数量。
注意,这个贪心算法假设硬币的面额是有规律的(例如美国硬币系统),在这种情况下,它总是能给出最少的硬币数量。但对于其他面额的组合,贪心算法可能不会总是给出最优解。例如,如果硬币面额为 [1, 3, 4]
并且需要找零 6
,贪心算法会选择两个面额为 3
的硬币,但实际上最优解是两个面额为 3
和一个面额为 1
的硬币。
例题: leetcode 5.最长回文字符串
https://leetcode.cn/problems/longest-palindromic-substring
class Solution:
def longestPalindrome(self, s: str) -> str:
# 解题思路1:暴力解法通过70/140,剩余超时,思考超时原因如下:如果s[1:8]不是回文串,就没必要继续看s[1:9]和之后的了,但是暴力没有解决这个问题,还有一些边界也没解决,如果挨个解决太麻烦,所以换个思路。
# 解题思路2:中心扩展,从左到右遍历s,以当前位置curr为原点,左右扩展,查看最长回文串,分为奇数回文和偶数回文,这样的好处是,如果s[2:3]不是回文串,就不会继续检查[1:4]或者[2:4】之类的。时间复杂度更低。
len_s = len(s)
if len_s == 1:
return s
max_len = 1
max_str = s[0]
for idx, val in enumerate(s):
# 查找偶数回文串,通过率75/142
left = idx
right = idx + 1
while left >= 0 and right < len_s:
if s[left] == s[right]:
temp_len = right + 1 - left
if temp_len > max_len:
max_len = temp_len
max_str = s[left:right + 1]
left -= 1
right += 1
else:
break
# 查找奇数回文串 通过率74/142
left = idx - 1
right = idx + 1
while left >= 0 and right < len_s:
if s[left] == s[right]:
temp_len = right + 1 - left
if temp_len > max_len:
max_len = temp_len
max_str = s[left:right + 1]
left -= 1
right += 1
else:
break
# 两种合起来通过
return max_str