贪心算法,又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。[1]比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。在不同情况,选择最优的解,可能会导致辛普森悖论,不一定出现最优的解。
这里,我们先使用一个找零钱的例子来模拟这个算法
找零钱问题假设你开了间小店,不能电子支付,钱柜里的货币只有 25 分、10 分、5 分和 1 分四种硬币,如果你是售货员且要找给客户 41 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少?这里需要明确的几个点:1.货币只有 25 分、10 分、5 分和 1 分四种硬币;2.找给客户 41 分钱的硬币;3.硬币最少化思考,能使用我们今天学到的贪婪算法吗?怎么做?
这里我们采用python来实现这个算法
#!/usr/bin/python3 # -*- coding: UTF-8 -*- __author__ = "A.L.Kun" __file__ = "test.py" __time__ = "2022/7/24 12:16" coin = [25, 10, 1, 5] # 所有类型的硬币 coin.sort() # 先对硬币的面值进行排序,后面采用出栈的形式来获取数据 # 定一个字典,存储使用了多少硬币 dic = {25: 0, 10: 0, 5: 0, 1: 0} # 初始化字典 # 确定初始解 value = 41 - coin[-1] # 初始条件为减去一个最大的值 dic[coin[-1]] += 1 # 这个也要自加1 while value > 0 and coin: # 如果我们把钱找完,并且可以把 if value - coin[-1] >= 0: value -= coin[-1] # 对面值进行相减 dic[coin[-1]] += 1 # 计数中自加1 else: coin.pop() # 如果不能再相减的话,就把这个硬币给弹出,不在考虑 print(dic) # 最后我们输出结果
这里,我们使用的题目是2005年的数学建模大赛B,第二问:
表2中列出了网站手上100种DVD的现有张数和当前需要处理的1000位会员的在线订单(表2的数据格式示例如下表2,具体数据请从http://mcm.edu.cn/mcm05/problems2005c.asp下载),如何对这些DVD进行分配,才能使会员获得最大的满意度?请具体列出前30位会员(即C0001~C0030)分别获得哪些DVD。
数据请自己从网上下载哦!
表2:
DVD编号 | D001 | D002 | D003 | D004 | … |
---|---|---|---|---|---|
DVD现有数量 | 10 | 40 | 15 | 20 | … |
C0001 | 6 | 0 | 0 | 0 | … |
C0002 | 0 | 0 | 0 | 0 | … |
C0003 | 0 | 0 | 0 | 3 | … |
C0004 | 0 | 0 | 0 | 0 | … |
… | … | … | … | … | … |
这里,我展示一下,我对这个贪心算法的理解,展示一下我的代码,有问题请多多指正:
这里,我们要先把Excel中的数据清洗出来,这样我们更好处理:
def init_data( io: str = "./B2005Table2.xls" # 传入Excel表格的路径 ) -> (defaultdict, dict): # 初始化数据的函数 """读取Excel中的数据,并且对数据进行清洗""" data_num = defaultdict(dict) # 存存储成员的对所有数据的喜爱程度 df = pd.read_excel(io) # 读取Excel数据 data = df[0:].iloc[:, 1:] # 获取到关于所有的用户愿意观看的程度,以及所有的用户信息 # 初始化DVD的全部信息 col_index = data.columns[1:].tolist() # 获取到每一种DVD capacity = data.loc[0, col_index].tolist() # 获取到每一种DVD有多少张 dic_goods = dict(zip(col_index, capacity)) # 转换为映射关系,更好访问 # 初始化会员的全部信息 for col_index_ in col_index: # 得到每一列 for row_index in data.index[1:]: # 遍历每一列 temp = dict() # 设置临时变量 name = data.loc[row_index, "Unnamed: 1"] # 获取名字 satisfaction = 40 - int(data.loc[row_index, col_index_]) if data.loc[ row_index, col_index_] else 0.0 # 设置满意度 if data_num.get(name): # 判断是否存在name,如果存在 temp = data_num[name] # 把原来的数据拷贝出来 temp[col_index_] = satisfaction # 把新的满意度存入 data_num.update({ name: temp }) # 更新数据 return data_num, dic_goods # 返回获取到的满意度映射表,以及商品余量映射表
def get_max( dic: dict, # 满意度映射表 ): """获取最大值""" max_ = list(dic.keys())[0] # 默认第一个键值对为最大值 for key, value in dic.items(): if value > dic[max_]: max_ = key return max_ # 返回最大值 def distribution( data_num: defaultdict, # 满意度映射表 dic_goods: dict # 商品余量映射表 ): """满足每一个会员的优先需求""" for name, dic in data_num.items(): # 遍历每一个字典 temp_dic = dic.copy() # 防止其修改原来字典的值 max_ = get_max(temp_dic) # 获取最大值 value = temp_dic[max_] # 进行对while循环的判断 while temp_dic and value: # 如果最终没货的话,就不用管了,同时,如果把每个人的感兴趣的东西选完,就没必要去选了,这个while循环就可以对成员对的多个满意度进行迭代,然后进行光盘的分配 max_ = get_max(temp_dic) # 获取最大值、 value = temp_dic[max_] # 进行对while循环的判断 temp_dic.pop(max_) # 获取里面最大的字典,并把这个键值对删除,但是不会删除主字典里面的值 if len(selected_con[name]) < 3: # 最基本的条件 if dic_goods[max_] > 0: # 如果DVD有货,并且用户没有获得3个DVD selected_con[name].append(max_) # 把光盘添加到主容器中 dic_goods[max_] -= 1 # 把商品数据去除 else: # 如果满了的话,就不用考虑了 break
剩余的没有分配到DVD的会员,我们需要重新对其进行随机分配,因为剩余的会员大部分都是没有获取到喜好的类型的DVD,对剩余DVD的兴趣为0,我们在这里可以随机对其进行分配,直到每个人都拥有DVD
def validation( dic_goods: dict # 商品余量映射表 ): """对没有分满的会员进行随机分配""" unselected = [] # 存储没有分满的会员 for name, lis in selected_con.items(): # 获取出没有不满的人 if len(lis) < 3: unselected.append({ "name": name, # 存储没有补满的会员名单 "count": 3 - len(lis) # 保存剩余的商品数量 }) # 对没有分满的人进行随机补全 for dic in unselected: # 对每个会员进行添加数据 for index in range(dic["count"]): # 补全剩余的商品数量 while True: # 添加的数据进行随机化 add_name = random.choice(list(dic_goods.keys())) # 随机获取要添加的人 if dic_goods[add_name] > 0: selected_con[dic["name"]].append(add_name) # 添加数据 dic_goods[add_name] -= 1 # 自减1 break
运算完后,我们需要将数据写入Excel文件中,进行分析和存储:
def write_to_excel( satisfaction: dict, # 满意度映射表 io_selected: str = "./selected.xlsx", # 存储路径 ): """将得到DVD的成员写入Excel中""" book = Workbook() sheet = book.active sheet.title = "已分配到DVD的成员" sheet.cell(1, 1).value = '序号' sheet.cell(1, 2).value = '名字' sheet.cell(1, 3).value = 'DVD_1' sheet.cell(1, 4).value = 'DVD_2' sheet.cell(1, 5).value = 'DVD_3' row_index = 1 index = 0 for name, lis in selected_con.items(): sheet.cell(row_index + 1, 1).value = index + 1 sheet.cell(row_index + 1, 2).value = name lis_0 = f"{lis[0]} / {satisfaction[name][lis[0]]}" # 类型 / 满意度 sheet.cell(row_index + 1, 3).value = lis_0 lis_1 = f"{lis[1]} / {satisfaction[name][lis[1]]}" sheet.cell(row_index + 1, 4).value = lis_1 lis_2 = f"{lis[2]} / {satisfaction[name][lis[2]]}" sheet.cell(row_index + 1, 5).value = lis_2 get_sum = satisfaction[name][lis[0]] + satisfaction[name][lis[1]] + satisfaction[name][lis[2]] row_index += 1 index += 1 book.save(io_selected)
#!/usr/bin/python3 # -*- coding: UTF-8 -*- __author__ = "A.L.Kun" __file__ = "demo2.py" __time__ = "2022/7/24 16:45" import pandas as pd from collections import defaultdict from openpyxl import Workbook import random selected_con = defaultdict(list) # 存储筛选出来的用户 def init_data( io: str = "./B2005Table2.xls" # 传入Excel表格的路径 ) -> (defaultdict, dict): # 初始化数据的函数 """读取Excel中的数据,并且对数据进行清洗""" data_num = defaultdict(dict) # 存存储成员的对所有数据的喜爱程度 df = pd.read_excel(io) # 读取Excel数据 data = df[0:].iloc[:, 1:] # 获取到关于所有的用户愿意观看的程度,以及所有的用户信息 # 初始化DVD的全部信息 col_index = data.columns[1:].tolist() # 获取到每一种DVD capacity = data.loc[0, col_index].tolist() # 获取到每一种DVD有多少张 dic_goods = dict(zip(col_index, capacity)) # 转换为映射关系,更好访问 # 初始化会员的全部信息 for col_index_ in col_index: # 得到每一列 for row_index in data.index[1:]: # 遍历每一列 temp = dict() # 设置临时变量 name = data.loc[row_index, "Unnamed: 1"] # 获取名字 satisfaction = 40 - int(data.loc[row_index, col_index_]) if data.loc[ row_index, col_index_] else 0.0 # 设置满意度 if data_num.get(name): # 判断是否存在name,如果存在 temp = data_num[name] # 把原来的数据拷贝出来 temp[col_index_] = satisfaction # 把新的满意度存入 data_num.update({ name: temp }) # 更新数据 return data_num, dic_goods # 返回获取到的满意度映射表,以及商品余量映射表 def get_max( dic: dict, # 满意度映射表 ): """获取最大值""" max_ = list(dic.keys())[0] # 默认第一个键值对为最大值 for key, value in dic.items(): if value > dic[max_]: max_ = key return max_ # 返回最大值 def distribution( data_num: defaultdict, # 满意度映射表 dic_goods: dict # 商品余量映射表 ): """满足每一个会员的优先需求""" for name, dic in data_num.items(): # 遍历每一个字典 temp_dic = dic.copy() # 防止其修改原来字典的值 max_ = get_max(temp_dic) # 获取最大值 value = temp_dic[max_] # 进行对while循环的判断 while temp_dic and value: # 如果最终没货的话,就不用管了,同时,如果把每个人的感兴趣的东西选完,就没必要去选了 max_ = get_max(temp_dic) # 获取最大值、 value = temp_dic[max_] # 进行对while循环的判断 temp_dic.pop(max_) # 获取里面最大的字典,并把这个键值对删除,但是不会删除主字典里面的值 if len(selected_con[name]) < 3: # 最基本的条件 if dic_goods[max_] > 0: # 如果DVD有货,并且用户没有获得3个DVD selected_con[name].append(max_) # 把光盘添加到主容器中 dic_goods[max_] -= 1 # 把商品数据去除 else: # 如果满了的话,就不用考虑了 break def write_to_excel( satisfaction: dict, # 满意度映射表 io_selected: str = "./selected.xlsx", # 存储路径 ): """将得到DVD的成员写入Excel中""" book = Workbook() sheet = book.active sheet.title = "已分配到DVD的成员" sheet.cell(1, 1).value = '序号' sheet.cell(1, 2).value = '名字' sheet.cell(1, 3).value = 'DVD_1' sheet.cell(1, 4).value = 'DVD_2' sheet.cell(1, 5).value = 'DVD_3' row_index = 1 index = 0 for name, lis in selected_con.items(): sheet.cell(row_index + 1, 1).value = index + 1 sheet.cell(row_index + 1, 2).value = name lis_0 = f"{lis[0]} / {satisfaction[name][lis[0]]}" # 类型 / 满意度 sheet.cell(row_index + 1, 3).value = lis_0 lis_1 = f"{lis[1]} / {satisfaction[name][lis[1]]}" sheet.cell(row_index + 1, 4).value = lis_1 lis_2 = f"{lis[2]} / {satisfaction[name][lis[2]]}" sheet.cell(row_index + 1, 5).value = lis_2 get_sum = satisfaction[name][lis[0]] + satisfaction[name][lis[1]] + satisfaction[name][lis[2]] row_index += 1 index += 1 book.save(io_selected) def validation( dic_goods: dict # 商品余量映射表 ): """对没有分满的会员进行随机分配""" unselected = [] # 存储没有分满的会员 for name, lis in selected_con.items(): # 获取出没有不满的人 if len(lis) < 3: unselected.append({ "name": name, # 存储没有补满的会员名单 "count": 3 - len(lis) # 保存剩余的商品数量 }) # 对没有分满的人进行随机补全 for dic in unselected: # 对每个会员进行添加数据 for index in range(dic["count"]): # 补全剩余的商品数量 while True: # 添加的数据进行随机化 add_name = random.choice(list(dic_goods.keys())) # 随机获取要添加的人 if dic_goods[add_name] > 0: selected_con[dic["name"]].append(add_name) # 添加数据 dic_goods[add_name] -= 1 # 自减1 break def main(): data_num, dic_goods = init_data() # 初始化数据 distribution(data_num, dic_goods) # 初始化数据,同时获取结果 validation(dic_goods) # 那么剩余的都是一些没有分配到喜欢的DVD的对象了 write_to_excel(data_num) # 写入Excel表中 if __name__ == '__main__': main()