python实现贪心算法

贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的是在某种意义上的局部最优解。

1. 贪心算法思想

贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止。

2.贪心算法的过程:

1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每一子问题求解,得到子问题的局部最优解
4.把子问题的解局部最优解合成原来解问题的一个解

算法求解过程是首先从某一解向目标值出发,得到可行解的元素,然后合成所有解元素而得到一个可行解。

3.贪心算法的不足

贪心算法的解题方式是从可选的第一个解开始逐步到达目标解,如果在寻解的过程中因某种条件限制而停止向前,就得到一个近似解,因此贪心算法存在以下不足:
1)贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
2) 不适用于最值问题

4.贪心选择的定义

贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。

5.最优子结构的定义

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题 。

经典例题

1)给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。(leetcode题目)

注意:
num 的长度小于 1000 且 ≥ k。
num 不会包含任何前导零。
示例 1 :
输入: num = “1432219”, k = 3
输出: “1219”
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。

解题思路:
贪心 + 单调栈
对于两个相同长度的数字序列,最左边不同的数字决定了这两个数字的大小,例如,对于 A=1axxxA = 1axxxA=1axxx,B=1bxxxB = 1bxxxB=1bxxx,如果 a>ba > ba>b 则 A>BA > BA>B。
基于此,我们可以知道,若要使得剩下的数字最小,需要保证靠前的数字尽可能小。
在这里插入图片描述
给定一个数字序列,例如 425,如果要求我们只删除一个数字,那么从左到右,我们有 4、2 和 5 三个选择。我们将每一个数字和它的左邻居进行比较。从 2 开始,2 小于它的左邻居 4。假设我们保留数字 4,那么所有可能的组合都是以数字 4(即 42,45)开头的。相反,如果移掉 4,留下 2,我们得到的是以 2开头的组合(即 25),这明显小于任何留下数字 4 的组合。因此我们应该移掉数字 4。如果不移掉数字 4,则之后无论移掉什么数字,都不会得到最小数。

基于此,我们可以每次对整个数字序列执行一次这个策略;删去一个字符后,剩下的 n−1 长度的数字序列就形成了新的子问题,可以继续使用同样的策略,直至删除 k 次。
我们可以用一个栈维护当前的答案序列,栈中的元素代表截止到当前位置,删除不超过 k 次个数字后,所能得到的最小整数。根据之前的讨论:在使用 k 个删除次数之前,栈中的序列从栈底到栈顶单调不降。

因此,对于每个数字,如果该数字小于栈顶元素,我们就不断地弹出栈顶元素,直到:
1.栈为空
2.或者新的栈顶元素不大于当前数字
3.或者我们已经删除了 k 位数字

上述步骤结束后我们还需要针对一些情况做额外的处理:

  1. 如果我们删除了 m 个数字且 m<k,这种情况下我们需要从序列尾部删除额外的 k−m 个数字。
    2.如果最终的数字序列存在前导零,我们要删去前导零。
    3. 如果最终数字序列为空,我们应该返回 0。

最终,从栈底到栈顶的答案序列即为最小数。

def removeKdigits(num, k):
    #辅助栈
    numStack = []
    # 构建单调递增的数字串
    for digit in num:
        while k and numStack and numStack[-1] > digit:
            numStack.pop()
            k -= 1

        numStack.append(digit)

    # 如果 K > 0,删除末尾的 K 个字符
    finalStack = numStack[:-k] if k else numStack

    # 抹去前导零
    return "".join(finalStack).lstrip('0') or "0"

2)找零钱问题(Coin Change Problem):给定一些面额不同的硬币和一个总金额,找到最少的硬币数目,使得它们的总值等于给定的金额

1.首先,我们将硬币面额按照从大到小的顺序排序。
2.然后,从最大面额的硬币开始,尽可能多地使用这个硬币,直到总金额减少到0或者没有更大面额的硬币可以使用。
3.接着,再尝试使用次大面额的硬币,重复以上步骤,直到总金额为0或者所有硬币都尝试过。
4.返回使用的硬币数量。

def coin_change(coins, amount):
    coins.sort(reverse=True)  # 按照面额从大到小排序
    num_coins = 0
    for coin in coins:
        num_coins += amount // coin  # 使用尽可能多的当前面额的硬币
        amount %= coin
    if amount == 0:
        return num_coins
    else:
        return -1  # 无法凑出总金额

# 示例用法
coins = [1, 2, 5, 10, 20, 50, 100]
amount = 123
print("最少硬币数量:", coin_change(coins, amount))

3)背包问题的近似解法(Fractional Knapsack Problem):给定一组物品和一个背包,每个物品都有一个重量和一个价值,找到一种方式将物品放入背包中,使得背包中物品的总价值最大。
1.首先,计算每个物品的单位价值(价值除以重量)。
2.然后,按照单位价值从大到小对物品排序。
3.从单位价值最高的物品开始,依次尽可能多地装入背包,直到背包装满或者物品用完。
4.返回背包中物品的总价值。

def fractional_knapsack(items, capacity):
    items.sort(key=lambda x: x[1] / x[0], reverse=True)  # 按照单位价值从大到小排序
    total_value = 0
    for weight, value in items:
        if capacity >= weight:
            total_value += value
            capacity -= weight
        else:
            total_value += value * (capacity / weight)
            break
    return total_value

# 示例用法
items = [(10, 60), (20, 100), (30, 120)]
capacity = 50
print("背包中物品的总价值:", fractional_knapsack(items, capacity))

4)活动选择问题(Activity Selection Problem):给定一组活动和每个活动的开始时间和结束时间,找到最大数量的互不相交的活动
1/首先,对所有活动按照结束时间进行排序。
2.选择第一个活动,并将其加入最终选择的列表中。
3.依次遍历剩余的活动,如果当前活动的开始时间晚于或等于前一个已选择活动的结束时间,则将该活动加入最终选择的列表中。
4.返回最终选择的活动列表。

def activity_selection(activities):
    activities.sort(key=lambda x: x[1])  # 按照结束时间从小到大排序
    selected_activities = [activities[0]]  # 第一个活动必选
    for activity in activities[1:]:
        if activity[0] >= selected_activities[-1][1]:  # 当前活动的开始时间晚于或等于前一个活动的结束时间
            selected_activities.append(activity)
    return selected_activities

# 示例用法
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 8), (5, 9), (6, 10), (8, 11), (8, 12), (2, 13), (12, 14)]
print("最大数量的互不相交的活动:", activity_selection(activities))

5)霍夫曼编码(Huffman Coding):一种用于数据压缩的编码方法,通过根据字符出现的频率构建一个最优的二叉树,然后根据该树对字符进行编码。
霍夫曼编码(Huffman Coding)实现思路:
1.首先,根据字符出现的频率构建最小堆。
2.依次从堆中弹出两个频率最低的节点,并将它们合并为一个新的节点,新节点的频率为两个节点的频率之和。
3.将新节点重新插入最小堆中。
4.重复步骤2和步骤3,直到最小堆中只剩下一个节点。
5.构建霍夫曼树,并根据树的结构生成字符的编码。

import heapq
from collections import defaultdict

def huffman_coding(freq):
    heap = [[weight, [char, ""]] for char, weight in freq.items()]
    heapq.heapify(heap)
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        for pair in left[1:]:
            pair[1] = '0' + pair[1]
        for pair in right[1:]:
            pair[1] = '1' + pair[1]
        heapq.heappush(heap, [left[0] + right[0]] + left[1:] + right[1:])
    return heap[0][1:]

# 示例用法
freq = {'a': 5, 'b': 9, 'c': 12, 'd': 13, 'e': 16, 'f': 45}
huffman_codes = huffman_coding(freq)
print("霍夫曼编码表:", huffman_codes)

6)最小生成树(Minimum Spanning Tree):给定一个带权的无向图,找到一个边的子集,使得所有顶点都连通,并且总权值最小。
1.首先,选择一个顶点作为起始点,将它加入最小生成树。
2.然后,从已选择的顶点集合中找到与之相连的边中权值最小的边,并将连接的顶点加入最小生成树。
3.重复步骤2,直到所有顶点都被加入最小生成树,或者最小生成树的边数等于顶点数减1为止。

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = []

    def add_edge(self, u, v, w):
        self.graph.append([u, v, w])

    def find(self, parent, i):
        if parent[i] == i:
            return i
        return self.find(parent, parent[i])

    def union(self, parent, rank, x, y):
        xroot = self.find(parent, x)
        yroot = self.find(parent, y)
        if rank[xroot] < rank[yroot]:
            parent[xroot] = yroot
        elif rank[xroot] > rank[yroot]:
            parent[yroot] = xroot
        else:
            parent[yroot] = xroot
            rank[xroot] += 1

    def kruskal_mst(self):
        result = []
        i = 0
        e = 0
        self.graph = sorted(self.graph, key=lambda item: item[2])
        parent = []
        rank = []
        for node in range(self.V):
            parent.append(node)
            rank.append(0)
        while e < self.V - 1:
            u, v, w = self.graph[i]
            i = i + 1
            x = self.find(parent, u)
            y = self.find(parent, v)
            if x != y:
                e = e + 1
                result.append([u, v, w])
                self.union(parent, rank, x, y)
        return result

# 示例用法
g = Graph(4)
g.add_edge(0, 1, 10)
g.add_edge(0, 2, 6)
g.add_edge(0, 3, 5)
g.add_edge(1, 3, 15)
g.add_edge(2, 3, 4)
print("最小生成树的边集合:", g.kruskal_mst())

7)最短路径问题(Shortest Path Problem):寻找图中两个顶点之间最短路径的问题,例如Dijkstra算法就是一种贪心算法,用于解决带权重的有向图或者无向图中的单源最短路径问题。
1.初始化距离数组,将源点到每个顶点的距离初始化为无穷大,将源点的距离初始化为0。
2.使用一个优先队列(最小堆)存储顶点和到源点的距离。
3.从优先队列中弹出一个顶点,更新与该顶点相邻的顶点的距离。
4.重复步骤3,直到优先队列为空。
Python 实现(Dijkstra算法):

def dijkstra(graph, src):
    distance = {node: float('inf') for node in graph}
    distance[src] = 0
    priority_queue = [(0, src)]
    while priority_queue:
        dist_u, u = heapq.heappop(priority_queue)
        if dist_u > distance[u]:
            continue
        for v, weight in graph[u].items():
            dist_v = dist_u + weight
            if dist_v < distance[v]:
                distance[v] = dist_v
                heapq.heappush(priority_queue, (dist_v, v))
    return distance

# 示例用法
graph = {
    'A': {'B': 6, 'C': 3},
    'B': {'A': 6, 'C': 2, 'D': 5},
    'C': {'A': 3, 'B': 2, 'D': 3},
    'D': {'B': 5, 'C': 3}
}
src = 'A'
print("从源点到各顶点的最短距离:", dijkstra(graph, src))

8)任务调度问题(Task Scheduling Problem):给定一组任务和每个任务的执行时间和截止时间,找到一种最优的调度方案,使得尽可能多的任务能够按时完成。
1.首先,按照任务的截止时间对任务进行排序。
2.选择当前时间最早的任务,将其加入到调度列表中,并更新当前时间。
3.如果当前时间超过了任务的截止时间,则放弃该任务。
4.重复步骤2和步骤3,直到所有任务都被处理完毕。

def task_scheduling(tasks):
    tasks.sort(key=lambda x: x[1])  # 按照截止时间排序
    schedule = []
    current_time = 0
    for task in tasks:
        if current_time < task[1]:  # 任务截止时间未到
            schedule.append(task)
            current_time += task[0]  # 更新当前时间
    return schedule


# 示例用法
tasks = [(3, 5), (1, 3), (2, 6), (4, 7), (5, 8)]
print("任务调度结果:", task_scheduling(tasks))

9)区间覆盖问题(Interval Covering Problem):给定一组区间,找到最少数量的区间,使得它们的并集覆盖了所有给定的区间。

1.首先,将所有区间按照起始位置进行排序。
2.选择第一个区间,并将其加入到覆盖列表中。
3.选择下一个未覆盖的区间,如果它的起始位置在当前覆盖列表的最后一个区间的终止位置之后,则将该区间加入到覆盖列表中。
4.重复步骤3,直到所有区间都被处理完毕。

def interval_covering(intervals):
    intervals.sort(key=lambda x: x[0])  # 按照起始位置排序
    covering = []
    current_end = float('-inf')
    for interval in intervals:
        if interval[0] > current_end:
            covering.append(interval)
            current_end = interval[1]
    return covering

# 示例用法
intervals = [(1, 3), (2, 4), (3, 6), (5, 7), (8, 10)]
print("区间覆盖结果:", interval_covering(intervals))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值