目录

贪心算法

一、 算法概述

1、 简介

贪心算法,又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。[1]比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。

贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。

贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。

贪心法可以解决一些最优化问题,如:求中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。在不同情况,选择最优的解,可能会导致辛普森悖论,不一定出现最优的解。

2、 基本步骤

  1. 从某个初始解出发;
  2. 采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一部分解,缩小问题规模;
  3. 将所有解综合起来。

二、 基本实现

1、 实例

这里,我们先使用一个找零钱的例子来模拟这个算法

找零钱问题假设你开了间小店,不能电子支付,钱柜里的货币只有 25 分、10 分、5 分和 1 分四种硬币,如果你是售货员且要找给客户 41 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少?这里需要明确的几个点:1.货币只有 25 分、10 分、5 分和 1 分四种硬币;2.找给客户 41 分钱的硬币;3.硬币最少化思考,能使用我们今天学到的贪婪算法吗?怎么做?

2、 分析步骤

  1. 初始解,我们可以从想找给顾客小于41中最大的硬币
  2. 使用迭代,不断的给客户找尽可能大的硬币,这样才能实现个数少的要求
  3. 将所有解综合起来

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)  # 最后我们输出结果

三、 数模实战

1、 题目展示

这里,我们使用的题目是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

2、 题目分析

2.1 公式

假设成员对每一个商品选择的序号为 \(a_{ij}\)

则,我们取成员满意度为\(d_{ij}\)

\[d_{ij} =\left\{\begin{matrix} \frac{1}{a_{ij}}, a_{ij} > 0 \\ 0, a_{ij}=0 \end{matrix}\right. \]

同时,如果会员得到其选择的前三个商品,则其满意度应该为100%,故,设总满意度为:

\[D_{0} = 1 + \frac{1}{2} + \frac{1}{3} \]

那么,成员获取到DVD后的满意度可以表示为

\[D_i = \frac{\sum_{j=0}^{3}{a_{ij}}}{D_0} \]

则,成员的总满意度可以表示为:

\[D = \frac{\sum_{i=1}^{n}{D_i}}{n},n\subseteq (1, 2,3,\cdots,1000) \]

2.2 实现

  1. 对于库存的100种光盘,首先满足所有对它偏爱顺序为1的会员的需要,即将每种光盘分配给所有对其偏爱顺序为1的会员,如果该光盘的数目偏少无法完成此次分配,则先分配给其中编号较小 的那些会员
  2. 对于剩余光盘,再优先满足对它偏爱顺序为2的会员需要,同样地,如果该光盘的数 目偏少无法完成此次分配,则先分配给其中编号较小的那些会员
  3. 依此类推分配下去,在以后分配时,已经拥有3张光盘的会员不参加分配
  4. 如果还有剩下的光盘,随机分配给尚未分满的会员,分配结束

3、 代码实现

这里,我展示一下,我对这个贪心算法的理解,展示一下我的代码,有问题请多多指正:

3.1 初始化数据

这里,我们要先把Excel中的数据清洗出来,这样我们更好处理:

sum_ = 0  # 总满意度之和
selected_con = defaultdict(list)  # 存储筛选出来的用户
all_user_data = defaultdict(list)  # 用来存储所有会员对DVD的满意度,满意度映射表
dic_goods = dict()  # 存放DVD的库存量,商品余量映射表
total_satis = 1 + 1 / 2 + 1 / 3  # 总满意度


def init_data(
        io: str = "./B2005Table2.xls"  # 传入Excel表格的路径
) -> (defaultdict, dict):  # 初始化数据的函数
    """读取Excel中的数据,并且对数据进行清洗"""

    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.update(dict(zip(col_index, capacity)))  # 转换为映射关系,更好访问

    # 初始化会员的全部信息
    for col_index_ in col_index:  # 得到每一列
        for row_index in data.index[1:]:  # 遍历每一列
            name = data.loc[row_index, "Unnamed: 1"]  # 获取名字
            satisfaction = 1 / int(data.loc[row_index, col_index_]) if data.loc[
                row_index, col_index_] else 0.0  # 设置成员满意度
            all_user_data[name].append({
                "name": col_index_,  # 存放DVD的类型
                "satisfaction": satisfaction  # 存放该类型的成员满意度
            })

    for name in all_user_data.keys():
        # 对列表里面的数据进行排序
        all_user_data[name].sort(key=lambda dic: dic["satisfaction"])  # 升序排序,可以使用pop来把成员数据弹出

3.2 分发DVD

主要思想

  • 把已经分到3张DVD的成员从容器中删除该成员信息,可以提高程序运行效率
  • 成员对某个DVD的满意度为0,不对其进行分配,因为可能有其他会员对该DVD感兴趣
  • 成员对某个DVD的满意度不为0,但是DVD不存在了,我们也可以将该DVD在成员的满意度中删除,提高程序运行效率
  • 在完成上述操作后,我们可以发现,贪心算法运行到最后,就只存在满意度为0的成员,其余的成员要么库存不足,要么分配完成。故,这可以作为我们算法结束的判断标志,结束循环
def judge_zero(
        user_data: defaultdict  # 传入深度拷贝后的字典,反正修改原数据
):
    """判断满意度映射表中,剩余成员对各类DVD的满意度是否都为0,如果都为0,则说明初步分配完成"""
    for lis in user_data.values():  # 遍历剩余成员组成的字典
        for dic in lis:  # 遍历满意度列表
            if dic["satisfaction"]:
                return True  # 如果找到一个不为0的,就返回正,继续循环
    return False  # 如果都为0,返回假,结束循环


def greedy(
        user_data: defaultdict  # 传入深度拷贝后的字典,反正修改原数据
):
    """使用贪心算法对DVD进行分配"""
    while judge_zero(user_data):  # 根据是否初步分配完,对DVD进行初步的分配
        del_lis = []  # 存储分配到3个DVD的人,将其删掉
        for u_name, lis in user_data.items():  # @u_name: 会员名
            name, satis = lis[-1]["name"], lis[-1]["satisfaction"]  # 由于排序为升序排序,故,最大的在最右边
            # @name:  DVD类型   @satis: 该类型满意度
            while satis:  # 当满意度不为0的时候才去分配,不然,可能会占用其他满意度不为零的DVD
                if len(selected_con[u_name]) < 3:  # 只有分配到的DVD小于3,才有机会分配到DVD
                    if dic_goods[name] > 0:  # 只有有库存的时候,才会去进行分配
                        selected_con[u_name].append(name)  # 将选择到的DVD添加到准备好的容器中
                        dic_goods[name] -= 1  # 库存减少1
                    user_data[u_name].pop()  # 该类型DVD没了或者是已经分配了该DVD,就将其进行删除,因为没了的话后面也分配不到,提高运行效率
                else:  # 如果这个人分配到的DVD有3个,就把这个人从分配名单中去除,提高程序运行效率
                    del_lis.append(u_name)  # 其不需要再进行分配了,将其删除
                break

        for name in del_lis:  # 删除对应的会员
            user_data.pop(name)

3.3 分配余量

剩余的没有分配到DVD的会员,我们需要重新对其进行随机分配,因为剩余的会员大部分都是没有获取到喜好的类型的DVD,对剩余DVD的兴趣为0,我们在这里可以随机对其进行分配,直到每个人都拥有DVD

def validation():
    """对没有分满的会员进行随机分配"""
    # 我们还要清除DVD剩余量为0的商品
    del_lis = []
    for name, number in dic_goods.items():
        if not number:  # 如果余量为0
            del_lis.append(name)

    for name in del_lis:
        dic_goods.pop(name)

    # 对没有分配到3个DVD的会员进行随机分配
    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

3.4 数据存储

运算完后,我们需要将数据写入Excel文件中,进行分析和存储:

def write_to_excel(
        io_selected: str = "./selected.xlsx",  # 存储路径
):
    """将得到DVD的成员写入Excel中"""
    global sum_  # 全局变量局部化
    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'
    sheet.cell(1, 6).value = "满意度"
    index, row_index = 0, 1
    for name, lis in selected_con.items():
        sheet.cell(row_index + 1, 1).value = index + 1
        sheet.cell(row_index + 1, 2).value = name
        satis_0, satis_1, satis_2 = 0, 0, 0  # 初始化变量
        for dic in all_user_data[name]:
            if dic['name'] == lis[0]:
                satis_0 = dic["satisfaction"]
            elif dic['name'] == lis[1]:
                satis_1 = dic["satisfaction"]
            elif dic['name'] == lis[2]:
                satis_2 = dic["satisfaction"]
        lis_0 = f"{lis[0]} / {satis_0 / total_satis:.2%}"  # 类型 / 满意度
        sheet.cell(row_index + 1, 3).value = lis_0
        lis_1 = f"{lis[1]} / {satis_1 / total_satis:.2%}"
        sheet.cell(row_index + 1, 4).value = lis_1
        lis_2 = f"{lis[2]} / {satis_2 / total_satis:.2%}"
        sheet.cell(row_index + 1, 5).value = lis_2
        local_sum = (satis_0 + satis_1 + satis_2) / total_satis
        sum_ += local_sum  # 求取满意度的总和
        sheet.cell(row_index + 1, 6).value = f"{local_sum:.2%}"
        row_index += 1
        index += 1
    sheet.cell(row_index + 1, 6).value = f"{sum_ / index:.2%}"  # 添加总满意度
    book.save(io_selected)

3.4 输出结果

问题中需要我们输出前30位会员的分配到的DVD信息

def pretty_out():
    """将前30名会员获取的DVD及其满意度输出"""
    count = 0  # 计数作用
    print("C0001~C0030 分配信息")
    table = PrettyTable(["会员", "DVD1", "DVD2", "DVD3", "满意度"])
    for name, lis in selected_con.items():
        satis_0, satis_1, satis_2 = 0, 0, 0  # 初始化变量
        for dic in all_user_data[name]:
            if dic['name'] == lis[0]:
                satis_0 = dic["satisfaction"]
            elif dic['name'] == lis[1]:
                satis_1 = dic["satisfaction"]
            elif dic['name'] == lis[2]:
                satis_2 = dic["satisfaction"]
        table.add_row([
            name,
            f"{lis[0]} / {satis_0 / total_satis:.2%}",
            f"{lis[1]} / {satis_1 / total_satis:.2%}",
            f"{lis[2]} / {satis_2 / total_satis:.2%}",
            f"{(satis_0 + satis_1 + satis_2) / total_satis:.2%}"
        ])
        count += 1
        if count > 29:
            break
    print(table)
    print(f"会员总满意度为:{sum_ / len(list(selected_con.keys())):.2%}")

4、 总代码

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
__author__ = "A.L.Kun"
__file__ = "demo3.py"
__time__ = "2022/7/26 9:41"

import pandas as pd
from collections import defaultdict
from openpyxl import Workbook
from copy import deepcopy
import random
from prettytable import PrettyTable

sum_ = 0  # 总满意度之和
selected_con = defaultdict(list)  # 存储筛选出来的用户
all_user_data = defaultdict(list)  # 用来存储所有会员对DVD的满意度,满意度映射表
dic_goods = dict()  # 存放DVD的库存量,商品余量映射表
total_satis = 1 + 1 / 2 + 1 / 3  # 总满意度


def init_data(
        io: str = "./B2005Table2.xls"  # 传入Excel表格的路径
) -> (defaultdict, dict):  # 初始化数据的函数
    """读取Excel中的数据,并且对数据进行清洗"""

    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.update(dict(zip(col_index, capacity)))  # 转换为映射关系,更好访问

    # 初始化会员的全部信息
    for col_index_ in col_index:  # 得到每一列
        for row_index in data.index[1:]:  # 遍历每一列
            name = data.loc[row_index, "Unnamed: 1"]  # 获取名字
            satisfaction = 1 / int(data.loc[row_index, col_index_]) if data.loc[
                row_index, col_index_] else 0.0  # 设置成员满意度
            all_user_data[name].append({
                "name": col_index_,  # 存放DVD的类型
                "satisfaction": satisfaction  # 存放该类型的成员满意度
            })

    for name in all_user_data.keys():
        # 对列表里面的数据进行排序
        all_user_data[name].sort(key=lambda dic: dic["satisfaction"])  # 升序排序,可以使用pop来把成员数据弹出


def judge_zero(
        user_data: defaultdict  # 传入深度拷贝后的字典,反正修改原数据
):
    """判断满意度映射表中,剩余成员对各类DVD的满意度是否都为0,如果都为0,则说明初步分配完成"""
    for lis in user_data.values():  # 遍历剩余成员组成的字典
        for dic in lis:  # 遍历满意度列表
            if dic["satisfaction"]:
                return True  # 如果找到一个不为0的,就返回正,继续循环
    return False  # 如果都为0,返回假,结束循环


def greedy(
        user_data: defaultdict  # 传入深度拷贝后的字典,反正修改原数据
):
    """使用贪心算法对DVD进行分配"""
    while judge_zero(user_data):  # 根据是否初步分配完,对DVD进行初步的分配
        del_lis = []  # 存储分配到3个DVD的人,将其删掉
        for u_name, lis in user_data.items():  # @u_name: 会员名
            name, satis = lis[-1]["name"], lis[-1]["satisfaction"]  # 由于排序为升序排序,故,最大的在最右边
            # @name:  DVD类型   @satis: 该类型满意度
            while satis:  # 当满意度不为0的时候才去分配,不然,可能会占用其他满意度不为零的DVD
                if len(selected_con[u_name]) < 3:  # 只有分配到的DVD小于3,才有机会分配到DVD
                    if dic_goods[name] > 0:  # 只有有库存的时候,才会去进行分配
                        selected_con[u_name].append(name)  # 将选择到的DVD添加到准备好的容器中
                        dic_goods[name] -= 1  # 库存减少1
                    user_data[u_name].pop()  # 该类型DVD没了或者是已经分配了该DVD,就将其进行删除,因为没了的话后面也分配不到,提高运行效率
                else:  # 如果这个人分配到的DVD有3个,就把这个人从分配名单中去除,提高程序运行效率
                    del_lis.append(u_name)  # 其不需要再进行分配了,将其删除
                break

        for name in del_lis:  # 删除对应的会员
            user_data.pop(name)


def validation():
    """对没有分满的会员进行随机分配"""
    # 我们还要清除DVD剩余量为0的商品
    del_lis = []
    for name, number in dic_goods.items():
        if not number:  # 如果余量为0
            del_lis.append(name)

    for name in del_lis:
        dic_goods.pop(name)

    # 对没有分配到3个DVD的会员进行随机分配
    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 write_to_excel(
        io_selected: str = "./selected.xlsx",  # 存储路径
):
    """将得到DVD的成员写入Excel中"""
    global sum_  # 全局变量局部化
    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'
    sheet.cell(1, 6).value = "满意度"
    index, row_index = 0, 1
    for name, lis in selected_con.items():
        sheet.cell(row_index + 1, 1).value = index + 1
        sheet.cell(row_index + 1, 2).value = name
        satis_0, satis_1, satis_2 = 0, 0, 0  # 初始化变量
        for dic in all_user_data[name]:
            if dic['name'] == lis[0]:
                satis_0 = dic["satisfaction"]
            elif dic['name'] == lis[1]:
                satis_1 = dic["satisfaction"]
            elif dic['name'] == lis[2]:
                satis_2 = dic["satisfaction"]
        lis_0 = f"{lis[0]} / {satis_0 / total_satis:.2%}"  # 类型 / 满意度
        sheet.cell(row_index + 1, 3).value = lis_0
        lis_1 = f"{lis[1]} / {satis_1 / total_satis:.2%}"
        sheet.cell(row_index + 1, 4).value = lis_1
        lis_2 = f"{lis[2]} / {satis_2 / total_satis:.2%}"
        sheet.cell(row_index + 1, 5).value = lis_2
        local_sum = (satis_0 + satis_1 + satis_2) / total_satis
        sum_ += local_sum  # 求取满意度的总和
        sheet.cell(row_index + 1, 6).value = f"{local_sum:.2%}"
        row_index += 1
        index += 1
    sheet.cell(row_index + 1, 6).value = f"{sum_ / index:.2%}"
    book.save(io_selected)


def pretty_out():
    """将前30名会员获取的DVD及其满意度输出"""
    count = 0  # 计数作用
    print("C0001~C0030 分配信息")
    table = PrettyTable(["会员", "DVD1", "DVD2", "DVD3", "满意度"])
    for name, lis in selected_con.items():
        satis_0, satis_1, satis_2 = 0, 0, 0  # 初始化变量
        for dic in all_user_data[name]:
            if dic['name'] == lis[0]:
                satis_0 = dic["satisfaction"]
            elif dic['name'] == lis[1]:
                satis_1 = dic["satisfaction"]
            elif dic['name'] == lis[2]:
                satis_2 = dic["satisfaction"]
        table.add_row([
            name,
            f"{lis[0]} / {satis_0 / total_satis:.2%}",
            f"{lis[1]} / {satis_1 / total_satis:.2%}",
            f"{lis[2]} / {satis_2 / total_satis:.2%}",
            f"{(satis_0 + satis_1 + satis_2) / total_satis:.2%}"
        ])
        count += 1
        if count > 29:
            break
    print(table)
    print(f"会员总满意度为:{sum_ / len(list(selected_con.keys())):.2%}")


def main():
    init_data()  # 初始化对象,并完成对会员选择的排序
    greedy(deepcopy(all_user_data))  # 进行初步的分配
    validation()  # 在初步分配完成后,说明剩余的会员都将无法获得其选择的DVD,最后我们进行随机分配
    write_to_excel()  # 写入Excel中
    pretty_out()  # 输出数据到控制台上


if __name__ == '__main__':
    main()