第十天——贪心算法——深度总结

文章目录

贪心算法深度解析:原理与应用

[1. 贪心算法的基本原理](#1. 贪心算法的基本原理)

[1.1 贪心选择性质](#1.1 贪心选择性质)

[1.2 最优子结构](#1.2 最优子结构)

[1.3 贪心算法与动态规划的对比](#1.3 贪心算法与动态规划的对比)

[2. 贪心算法的应用场景](#2. 贪心算法的应用场景)

[3. 具体应用案例](#3. 具体应用案例)

[3.1 分配饼干 (Assign Cookies)](#3.1 分配饼干 (Assign Cookies))

[3.2 分糖果 (Candy Distribution)](#3.2 分糖果 (Candy Distribution))

[3.3 种花问题 (Can Place Flowers)](#3.3 种花问题 (Can Place Flowers))

[3.4 区间问题 (Non-overlapping Intervals)](#3.4 区间问题 (Non-overlapping Intervals))

[3.5 射气球 (Minimum Number of Arrows to Burst Balloons)](#3.5 射气球 (Minimum Number of Arrows to Burst Balloons))

[3.6 广播台覆盖问题 (Set Covering Problem)](#3.6 广播台覆盖问题 (Set Covering Problem))

[3.7 硬币找零问题 (Coin Change - Greedy Approach)](#3.7 硬币找零问题 (Coin Change - Greedy Approach))

[3.8 股票买卖问题 (Best Time to Buy and Sell Stock II)](#3.8 股票买卖问题 (Best Time to Buy and Sell Stock II))

[3.9 分数背包问题 (Fractional Knapsack)](#3.9 分数背包问题 (Fractional Knapsack))

[3.10 字符串分割 (Partition Labels)](#3.10 字符串分割 (Partition Labels))

[3.11 队列重构问题 (Queue Reconstruction by Height)](#3.11 队列重构问题 (Queue Reconstruction by Height))

[3.12 非递减数组 (Non-decreasing Array)](#3.12 非递减数组 (Non-decreasing Array))

[3.13 迪克斯特拉算法 (Dijkstra's Algorithm)](#3.13 迪克斯特拉算法 (Dijkstra’s Algorithm))

[3.14 霍夫曼编码 (Huffman Coding)](#3.14 霍夫曼编码 (Huffman Coding))

[4. 总结](#4. 总结)


贪心算法深度解析:原理与应用

贪心算法(Greedy Algorithm)是一种简单而高效的算法设计策略,其核心思想是在每一步决策中选择当前最优解,期望通过一系列局部最优选择最终获得全局最优解。相比动态规划等方法,贪心算法通常更高效,但适用范围有限,需要问题具备特定的性质。本报告将系统介绍贪心算法的原理、应用场景,并通过14个由易到难的案例深入剖析其应用,帮助读者掌握其精髓并在实践中灵活运用。


1. 贪心算法的基本原理

1.1 贪心选择性质

贪心选择性质是贪心算法的核心,指每一步的局部最优选择能够逐步构建全局最优解。这种性质要求问题具有"无后效性",即当前选择不会影响后续决策的正确性。例如,在活动选择问题中,选择结束时间最早的活动不会限制后续选择。

1.2 最优子结构

最优子结构是指问题的最优解由子问题的最优解组成。在贪心算法中,通过不断缩小问题规模,最终能够得到全局最优解。例如,最小生成树问题中,每一步选择的边都会成为最终解的一部分。

1.3 贪心算法与动态规划的对比

  • 动态规划:通过记录所有子问题的解来构建全局最优解,时间复杂度较高(如 O(n²) 或更高)。
  • 贪心算法:直接选择当前最优解,不回溯,时间复杂度通常较低(如 O(n log n) 或 O(n))。
  • 局限性:贪心算法要求问题满足贪心选择性质,否则可能无法得到最优解。

2. 贪心算法的应用场景

贪心算法适用于以下常见领域:

  • 调度问题:如活动选择、任务调度。
  • 背包问题:如分数背包问题。
  • 图论问题:如最小生成树(Kruskal、Prim)、单源最短路径(Dijkstra)。
  • 编码与压缩:如霍夫曼编码。
  • 资源分配:如分配饼干、种花问题。

这些问题通常具备贪心选择性质和最优子结构,使得局部最优选择能够逐步逼近全局最优解。


3. 具体应用案例

以下是14个由易到难的贪心算法应用案例,每个案例包含问题描述、贪心策略、代码实现和分析,代码以 Python 实现并附有注释。

3.1 分配饼干 (Assign Cookies)

问题描述

有一群孩子和一堆饼干,每个孩子有饥饿度,每个饼干有饱腹度。每个孩子只能吃一个饼干,且饼干饱腹度需不小于孩子饥饿度。求最多有多少孩子能吃饱。

贪心策略

优先为饥饿度最小的孩子分配刚好能满足的最小饼干,最大化利用饼干资源。

代码实现

复制代码
def findContentChildren(children: list[int], cookies: list[int]):
    children.sort()  # 按饥饿度升序
    cookies.sort()   # 按饱腹度升序
    child_i, cookie_i = 0, 0
    while child_i < len(children) and cookie_i < len(cookies):
        if children[child_i] <= cookies[cookie_i]:  # 饼干能满足孩子
            child_i += 1  # 下一个孩子
        cookie_i += 1  # 下一块饼干
    return child_i

# 示例
print(findContentChildren([1, 2], [1, 2, 3]))  # 输出: 2

分析

通过对孩子和饼干排序,使用双指针技术从小到大分配,确保饼干资源高效利用。时间复杂度 O(n log n),空间复杂度 O(1)。


3.2 分糖果 (Candy Distribution)

问题描述

一群孩子站成一排,每个孩子有评分,评分高的孩子需比相邻孩子多糖果,每人至少一个,求最少糖果数。

贪心策略

两次遍历,从左到右和从右到左调整糖果数,满足相邻约束。

代码实现

复制代码
def candy(ratings: list[int]):
    n = len(ratings)
    candies = [1] * n  # 初始每人一个
    for i in range(1, n):  # 从左到右
        if ratings[i] > ratings[i - 1]:
            candies[i] = candies[i - 1] + 1
    for i in range(n - 1, 0, -1):  # 从右到左
        if ratings[i] < ratings[i - 1]:
            candies[i - 1] = max(candies[i - 1], candies[i] + 1)
    return sum(candies)

# 示例
print(candy([1, 0, 2]))  # 输出: 5

分析

两次遍历确保评分高的孩子糖果多,最小化总糖果数。时间复杂度 O(n),空间复杂度 O(n)。


3.3 种花问题 (Can Place Flowers)

问题描述

给定花坛数组 flowerbed(0 表示空,1 表示有花)和整数 n,判断能否种下 n 朵花,新种的花不能相邻。

贪心策略

尽可能早地在满足条件的位置种花,确保左右两侧为空(或在边界)。

代码实现

复制代码
def canPlaceFlowers(flowerbed, n):
    count = 0
    length = len(flowerbed)
    for i in range(length):
        if flowerbed[i] == 0:
            left_empty = (i == 0) or (flowerbed[i - 1] == 0)
            right_empty = (i == length - 1) or (flowerbed[i + 1] == 0)
            if left_empty and right_empty:
                flowerbed[i] = 1  # 种花
                count += 1
                if count >= n:
                    return True
    return count >= n

# 示例
print(canPlaceFlowers([1, 0, 0, 0, 1], 1))  # 输出: True

分析

通过线性扫描并在满足条件时种花,贪心策略最大化种植数量。时间复杂度 O(n),空间复杂度 O(1)。


3.4 区间问题 (Non-overlapping Intervals)

问题描述

给定多个区间,计算让它们互不重叠(起止相连不算重叠)所需移除的最少区间数。

贪心策略

按结束时间排序,优先保留结束最早的区间,为后续区间留出更多空间。

代码实现

复制代码
def eraseOverlapIntervals(intervals: list[list[int]]):
    intervals.sort(key=lambda x: x[1])  # 按结束时间排序
    removed, prev_end = 0, intervals[0][1]
    for i in range(1, len(intervals)):
        if prev_end > intervals[i][0]:  # 重叠,移除当前区间
            removed += 1
        else:
            prev_end = intervals[i][1]  # 更新结束时间
    return removed

# 示例
print(eraseOverlapIntervals([[1, 2], [2, 4], [1, 3]]))  # 输出: 1

分析

按结束时间排序后,选择不重叠的区间,贪心策略最小化移除数量。时间复杂度 O(n log n),空间复杂度 O(1)。


3.5 射气球 (Minimum Number of Arrows to Burst Balloons)

问题描述

给定气球的水平直径区间 points,求最少需要多少支箭射爆所有气球。

贪心策略

按结束位置排序,在最早结束的气球右端点射箭,覆盖尽可能多的气球。

代码实现

复制代码
def findMinArrowShots(points):
    if not points:
        return 0
    points.sort(key=lambda x: x[1])  # 按结束位置排序
    count = 1
    last_shot = points[0][1]
    for start, end in points:
        if start > last_shot:  # 不重叠,需新箭
            count += 1
            last_shot = end
    return count

# 示例
print(findMinArrowShots([[10, 16], [2, 8], [1, 6], [7, 12]]))  # 输出: 2

分析

通过在重叠区间的公共点射箭,贪心策略最小化箭数。时间复杂度 O(n log n),空间复杂度 O(1)。


3.6 广播台覆盖问题 (Set Covering Problem)

问题描述

给定需要覆盖的州和广播台覆盖范围,求覆盖所有州的最小广播台集合。

贪心策略

每次选择覆盖最多未覆盖州的广播台。

代码实现

复制代码
states_needed = set(["mt", "wa", "or", "id", "nv", "ut", "ca", "az"])
stations = {
    "kone": set(["id", "nv", "ut"]),
    "ktwo": set(["wa", "id", "mt"]),
    "kthree": set(["or", "nv", "ca"]),
    "kfour": set(["nv", "ut"]),
    "kfive": set(["ca", "az"])
}

final_stations = set()
while states_needed:
    best_station = None
    states_covered = set()
    for station, states in stations.items():
        covered = states_needed & states
        if len(covered) > len(states_covered):
            best_station = station
            states_covered = covered
    states_needed -= states_covered
    final_stations.add(best_station)

# 示例
print(final_stations)  # 输出: {'kone', 'ktwo', 'kthree', 'kfive'}

分析

通过贪心选择覆盖最多新州的广播台,近似求解最小覆盖问题。时间复杂度 O(nm),n 为广播台数,m 为州数。


3.7 硬币找零问题 (Coin Change - Greedy Approach)

问题描述

给定硬币面值 coins 和金额 amt,求凑出 amt 的最少硬币数,无法凑出返回 -1。

贪心策略

优先使用面值最大的硬币。

代码实现

复制代码
def coin_change_greedy(coins: list[int], amt: int) -> int:
    coins.sort(reverse=True)  # 按面值降序
    count = 0
    for coin in coins:
        while amt >= coin:
            amt -= coin
            count += 1
    return count if amt == 0 else -1

# 示例
print(coin_change_greedy([1, 3, 4], 6))  # 输出: 2 (但注意:并非总是最优)

分析

贪心策略在特定面值组合下有效,但如 [1, 3, 4] 和金额 6,贪心得 3(4+1+1),而最优为 2(3+3)。时间复杂度 O(n + k),k 为金额除以最小面值的次数。


3.8 股票买卖问题 (Best Time to Buy and Sell Stock II)

问题描述

给定每日股票价格,可多次买卖但不能同时持有多支股票,求最大利润。

贪心策略

在每个价格上涨期间买入并卖出,累加所有正收益。

代码实现

复制代码
def maxProfit(prices):
    max_profit = 0
    for i in range(1, len(prices)):
        if prices[i] > prices[i - 1]:  # 价格上涨
            max_profit += prices[i] - prices[i - 1]  # 累加利润
    return max_profit

# 示例
print(maxProfit([7, 1, 5, 3, 6, 4]))  # 输出: 7

分析

通过捕获所有正价格差,贪心策略确保最大利润。时间复杂度 O(n),空间复杂度 O(1)。


3.9 分数背包问题 (Fractional Knapsack)

问题描述

给定物品重量 wgt、价值 val 和背包容量 cap,可选择物品的一部分,求最大价值。

贪心策略

按单位重量价值排序,优先选择单位价值最高的物品。

代码实现

复制代码
class Item:
    def __init__(self, w: int, v: int):
        self.w = w
        self.v = v

def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
    items = [Item(w, v) for w, v in zip(wgt, val)]
    items.sort(key=lambda x: x.v / x.w, reverse=True)  # 按单位价值降序
    res = 0
    for item in items:
        if item.w <= cap:  # 整件放入
            res += item.v
            cap -= item.w
        else:  # 部分放入
            res += (item.v / item.w) * cap
            break
    return res

# 示例
print(fractional_knapsack([10, 20, 30], [60, 100, 120], 50))  # 输出: 240

分析

通过优先选择高单位价值的物品,贪心策略最大化背包价值。时间复杂度 O(n log n),空间复杂度 O(n)。


3.10 字符串分割 (Partition Labels)

问题描述

将字符串 s 分割成尽可能多的部分,每个字母只出现在一个部分中。

贪心策略

记录每个字母的最后位置,扩展当前部分至其中字母的最远位置。

代码实现

复制代码
def partition_labels(s):
    last_occurrence = {char: idx for idx, char in enumerate(s)}  # 字母最后位置
    start = end = 0
    result = []
    for i, char in enumerate(s):
        end = max(end, last_occurrence[char])  # 更新当前部分结束位置
        if i == end:  # 当前部分结束
            result.append(end - start + 1)
            start = end + 1
    return result

# 示例
print(partition_labels("ababcbacadefegdehijhklij"))  # 输出: [9, 7, 8]

分析

通过跟踪字母的最远位置,贪心策略最大化分割数量。时间复杂度 O(n),空间复杂度 O(k)(k 为字符集大小)。


3.11 队列重构问题 (Queue Reconstruction by Height)

问题描述

给定人群数组 people,每个元素为 [h, k](身高 h,前面有 k 个身高≥h 的人),重构队列。

贪心策略

按身高降序、k 升序排序,再按 k 值插入。

代码实现

复制代码
def reconstructQueue(people):
    people.sort(key=lambda x: (-x[0], x[1]))  # 身高降序,k 升序
    queue = []
    for p in people:
        queue.insert(p[1], p)  # 插入到 k 位置
    return queue

# 示例
print(reconstructQueue([[7, 0], [4, 4], [7, 1], [5, 0], [6, 1], [5, 2]]))  
# 输出: [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

分析

先安排高个子再插入矮个子,确保 k 值正确。时间复杂度 O(n²),空间复杂度 O(n)。


3.12 非递减数组 (Non-decreasing Array)

问题描述

判断数组是否可通过最多修改一个元素变为非递减数组。

贪心策略

遇到逆序时,优先调整当前元素以保持单调性。

代码实现

复制代码
def checkPossibility(nums):
    corrections = 0
    for i in range(len(nums) - 1):
        if nums[i] > nums[i + 1]:
            if i == 0 or nums[i - 1] <= nums[i + 1]:
                nums[i] = nums[i + 1]  # 降低当前值
            else:
                nums[i + 1] = nums[i]  # 提高后值
            corrections += 1
            if corrections > 1:
                return False
    return True

# 示例
print(checkPossibility([3, 4, 2, 3]))  # 输出: False

分析

通过局部调整,贪心策略以最小代价维持非递减性。时间复杂度 O(n),空间复杂度 O(1)。


3.13 迪克斯特拉算法 (Dijkstra's Algorithm)

问题描述

在带权有向图中,求从源点到所有顶点的最短路径。

贪心策略

每次选择当前距离最小的顶点,更新邻接顶点距离。

代码实现

复制代码
graph = {
    "start": {"a": 6, "b": 2},
    "a": {"fin": 1},
    "b": {"a": 3, "fin": 5},
    "fin": {}
}
infinity = float("inf")
costs = {"a": 6, "b": 2, "fin": infinity}
parents = {"a": "start", "b": "start", "fin": None}
processed = []

def find_lowest_cost_node(costs):
    lowest_cost = infinity
    lowest_cost_node = None
    for node in costs:
        cost = costs[node]
        if cost < lowest_cost and node not in processed:
            lowest_cost = cost
            lowest_cost_node = node
    return lowest_cost_node

node = find_lowest_cost_node(costs)
while node is not None:
    cost = costs[node]
    neighbors = graph[node]
    for n in neighbors.keys():
        new_cost = cost + neighbors[n]
        if costs[n] > new_cost:
            costs[n] = new_cost
            parents[n] = node
    processed.append(node)
    node = find_lowest_cost_node(costs)

# 示例
print(costs)  # 输出: {'a': 5, 'b': 2, 'fin': 6}

分析

通过贪心选择最小距离顶点,适用于无负权图。时间复杂度 O(V²),使用堆可优化至 O(E + V log V)。


3.14 霍夫曼编码 (Huffman Coding)

问题描述

构建最优前缀编码,实现数据压缩。

贪心策略

每次合并频率最低的两个节点,构建霍夫曼树。

代码实现

复制代码
import heapq
from collections import defaultdict

class HuffmanNode:
    def __init__(self, char=None, freq=0, left=None, right=None):
        self.char = char
        self.freq = freq
        self.left = left
        self.right = right
    def __lt__(self, other):
        return self.freq < other.freq

def build_frequency_table(text):
    frequency = defaultdict(int)
    for char in text:
        frequency[char] += 1
    return frequency

def build_huffman_tree(frequency):
    heap = []
    for char, freq in frequency.items():
        heapq.heappush(heap, HuffmanNode(char, freq))
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        merged = HuffmanNode(freq=left.freq + right.freq, left=left, right=right)
        heapq.heappush(heap, merged)
    return heapq.heappop(heap)

def build_code_dict(root, current_code, code_dict):
    if root is None:
        return
    if root.char is not None:
        code_dict[root.char] = current_code
        return
    build_code_dict(root.left, current_code + '0', code_dict)
    build_code_dict(root.right, current_code + '1', code_dict)

def huffman_encoding(text):
    if not text:
        return {}, None
    frequency = build_frequency_table(text)
    huffman_tree = build_huffman_tree(frequency)
    code_dict = {}
    build_code_dict(huffman_tree, '', code_dict)
    encoded_text = ''.join(code_dict[char] for char in text)
    return encoded_text, code_dict

# 示例
text = "huffman"
encoded_text, code_dict = huffman_encoding(text)
print(f"编码字典: {code_dict}")
print(f"编码结果: {encoded_text}")

分析

通过贪心合并低频节点,确保高频字符编码短,优化压缩率。时间复杂度 O(n log n),空间复杂度 O(n)。


4. 总结

贪心算法通过局部最优选择逼近全局最优解,适用于具备贪心选择性质和最优子结构的问题。其高效性和简洁性使其在调度、图论、资源分配等领域大放异彩。然而,其局限性在于并非所有问题都适用,使用前需验证问题性质。上述14个案例从简单到复杂,涵盖了贪心算法的多种应用场景,通过学习和实践,读者可深入理解其思想并灵活运用于实际问题。

相关推荐
Codeking__10 分钟前
”一维前缀和“算法原理及模板
数据结构·算法
休息一下接着来11 分钟前
C++ 条件变量与线程通知机制:std::condition_variable
开发语言·c++·算法
Code哈哈笑24 分钟前
【机器学习】支持向量回归(SVR)从入门到实战:原理、实现与优化指南
人工智能·算法·机器学习·回归·svm
努力学习的小廉35 分钟前
【C++】 —— 笔试刷题day_29
开发语言·c++·算法
小羊在奋斗36 分钟前
【LeetCode 热题 100】搜索插入位置 / 搜索旋转排序数组 / 寻找旋转排序数组中的最小值
算法·leetcode·职场和发展
meisongqing42 分钟前
【软件工程】符号执行与约束求解缺陷检测方法
人工智能·算法·软件工程·软件缺陷
莫叫石榴姐1 小时前
如何为大模型编写优雅且高效的提示词?
人工智能·算法
Echo``2 小时前
1:OpenCV—图像基础
c++·图像处理·人工智能·opencv·算法·计算机视觉·视觉检测
COOCC13 小时前
激活函数全解析:定义、分类与 17 种常用函数详解
人工智能·深度学习·神经网络·算法·机器学习·计算机视觉·自然语言处理
林下清风~3 小时前
力扣hot100——347.前K个高频元素(cpp手撕堆)
算法·leetcode·职场和发展