华中杯 A题简单复盘(附Python 源码)

文章目录

  • 华中杯 A题简单复盘(附Python 源码)
    • 前言
    • 题目简介
      • 问题背景
    • 题目以及思路
      • 分批算法设计
      • MindMap
        • 遗传算法优缺点
          • 优点
          • 缺点
        • 聚类算法优缺点
          • 优点
          • 缺点
        • 基于贪心的分批算法
          • 订单加入批次——贪心
          • 先大后小
      • 货品上架——遗传、贪心
      • 订单指派算法——贪心、并行分配
    • Code(Python)
    • 总结

前言

五一和队友们一起坐牢了打完了华中杯,只能说确实有点小折磨。这是虽然基本上都完成了题目,但是队内的共识还是觉得这次完成的不够好,所以我专门做了一次关于这次数模比赛的复盘,希望能够有所收获。

源码在最后边。

题目简介

这次的A题是 分拣系统优化问题

问题背景

电商公司配送中心的工作流程中,分拣环节的是影响配送中心整体性能的关键因素。

系统的流程如下

首先,系统统计汇总出当天全部待配送订单所包含的所有货品及相应数量。然后,转运工将这些货品由仓库转运至分拣处,并放置到货架上,等待分拣。而问题也做了简化,不需要考虑货架的承载量,一个货架只放置一种货品,对件数没有限制。最后,分拣工按任务单依次分拣出每一个订单的货品。

而题目就是在这样的背景下,需要我们对分拣系统做优化。

其实这个背景就划分了流程:分批、上架、分拣。

而下面的题目也是这样。

题目以及思路

具体题目和数据可以直接去官网查看“华中杯”大学生数学建模挑战赛 (hzbmmc.com)

分批算法设计

我们需要设计一个算法,实现将订单分批。货架的个数有限,而且每一个货架只能放置一种货物,当订单全部的货物总数超过货架数时,需要对订单进行分批。

算法的结果,需要计算附件中订单的信息在货架总数N为200时,输出最少的批次数,和分批中每批的订单数、货品种类数等信息。

MindMap

相信看完这道题目,很多人和我一样想到了遗传算法和聚类算法。的确遗传算法是适合寻求最优解、而聚类算法则是很适合利用样本空间中样本的相似度进行聚类。

而以下就是我对基于该题目遗传和聚类的分析。

遗传算法优缺点

优点

遗传算法的优点在于可以通过不断的迭代,筛选出优秀基因,针对这道题,我觉得其实就是筛选出优秀的订单的分批。

缺点

但是不同的是,我们在遗传算法的过程中,是有基因交换和基因变异两种情况的,这是遗传算法的筛选的途径。但是基因交换时,是将相同的位置(相对)的染色体进行交换,而我们如果是将不同批次的订单拆分后需要考虑的是交换后的新增种类数,而不是长度,所以单纯的遗传不一定适合。

聚类算法优缺点

优点

可以有效地将数据进行归类聚集。

缺点

就本题而言,聚类本身依靠的样本数据地相似程度,而该题需要的是新增种类,这显然不是可以通过改变相似程度的衡量方式可以处理的。直接聚类的结果可能会使得每个类都很聚集,但是不能使得类充分接近甚至达到货品种类数N的情况。

结合这两种算法,我提出了一种较为朴素的分批算法模型,算法的内核是贪心思想。

基于贪心的分批算法

分批过程,可以参考下图。

图片来自于我的队友(太强了,吹吹我的队友)

我们可以先将所有的订单按照种类数进行分类,划分大小订单。然后是根据大小订单,先用大订单快速生成批次,然后再从小订单中将订单加入批次。

关于这个模型,有几个重点。

订单加入批次——贪心

无论是大订单还是小订单,都先从已有的批次中选取可以添加当前订单且加入后批次新增种类数最小,这就是一个 贪心 的体现。

先大后小

我们在该模型中提出,需要先从大订单中选取,然后再选取小订单,为什么这样呢?

原因很简单,大订单因为自身货品的种类数较多,所以彼此之间种类的差异度会较大,我们可以利用这种特性,快速生成较多的批次,同时又能够使得生成的批次中种类的数量较多。

货品上架——遗传、贪心

在分好批次之后,针对每个批次,我们需要确定批次中货品的种类的上架顺序,根据每个订单的货品种类在该批次中的最大最小次序,有下列关于订单的拣选距离定义公式

我们需要设计出算法,使得批次中订单拣选距离的总和尽可能小。

我们参考了遗传算法中的基因交换的思路,我们先将批次中的货品的种类任意排序,然后经过多次迭代,每次迭代的过程:随机选取两个种类,如果种类交换次序后,批次中订单拣选距离相比原来拣选距离更小,我们就更新为新的次序。这里其实也用了贪心的思想,每次迭代保存局部最优解,然后用局部最优解去逼近全局最优解。

订单指派算法——贪心、并行分配

在分批、上架完成之后,我们需要对分拣工进行订单的指派,目的是使完成的时间尽可能小,每个分拣工的距离尽可能均衡。需要求取的结果是分拣工人数为5的时候的结果。

我们的算法是在每次优先选取移动距离最小的员工进行指派,然后优先选取Ta最近的订单,进行分配。

首先选取移动距离最小的员工这一点很好理解,因为该员工是所有员工中最先完成,所以需要优先分配,然后为了减少不必要的移动距离,需要给ta 分配最近的订单。

Code(Python)

import randomimport pandas as pd
import time# 这里定义一个全局变量,用来表示最大的货架数——即批次种类限制
N = 200# 然后是写一个函数,实现只存储同一订单对应的货品种类
def big_and_small(choose0, order):"""该函数用于在未保留的订单中划分大订单和小订单,大订单占1/3:param choose0::param order::return:"""big1 = []small0 = []choose0 = dict(choose0)# 选取的订单字典,键是订单号,值是种类数choose_order = {}for i1 in choose0:if choose0[i1] == 0:choose_order[i1] = len(order[i1])# 将未被选的订单字典排序choose_order = sorted(choose_order.items(), key=lambda x: x[1])new_len = len(choose_order)# 计算大小分界的索引mid_index = new_len * 2 // 3index0 = 0for i1 in choose_order:if index0 < mid_index:small0.append(i1[0])else:big1.append(i1[0])index0 += 1return big1, small0# 写一个函数判断是否在当前批次
def is_in(batch, order):"""其实应该是得出新增订单的新增货品种类数:param batch: 当前批订单, 内容是包含的货品:param order: 当前订单,也是货品:return: 新增货品种类数"""# 将批次和订单都列表化batch = list(batch)order = list(order)ans = 0# 循环累加不在当前批次的货品种类数for o0 in order:if o0 not in batch:ans += 1return ans# 写一个选取函数
def choose_batch(orders1, batch01, batch02, keys0):"""该函数用于在大订单集或者小订单集将这些订单实现分批引入洗牌算法,将选取的订单的顺序进行打乱随机:param orders1: 订单:param batch01: 批次列表1,存储批次对应的订单:param batch02: 批次列表2,存储批次对应的货品种类:param keys0: 选取订单的列表, 内容是订单号:return: 返回选取加入后的批次1和2"""# 写一个洗牌算法# 先获取选取订单的长度# keys_len = len(keys0)# # 逆序遍历选取列表# # 通过交换随机下标来实现将选取的订单的顺序洗牌# for k in range(keys_len - 1, -1, -1):#     # 获取随机下标#     rindex = random.randint(0, k)#     # 交换位置#     temp = keys0[rindex]#     keys0[rindex] = keys0[k]#     keys0[k] = tempbatch1 = dict(batch01)batch2 = dict(batch02)# 获取到当前最大批次数对应的下标i0 = len(batch1) - 1# 如果此时没有,这需要置为0if i0 < 0:i0 = 0for key0 in keys0:# 初始时需要创建批次if i0 not in batch1:batch1[i0] = []batch2[i0] = []add_type = -1index0 = -1flag = Falsefor j in range(i0 + 1):if key0 in batch1[j]:flag = Truebreakat = is_in(batch2[j], orders[key0])# 完全重合直接退出循环if at == 0:index0 = jbreakif (add_type < 0 or add_type >= at) and len(batch2[j]) + at <= N:add_type = atindex0 = jif flag:continueif index0 >= 0:batch1[index0].append(key0)for good in orders1[key0]:if good not in batch2[index0]:batch2[index0].append(good)else:i0 += 1batch1[i0] = []batch1[i0].append(key0)batch2[i0] = orders1[key0]return batch1, batch2# 然后接下来写一个从小订单中选取
def choose_small(orders1, batch1, batch2, small1):orders1 = dict(orders1)keys = list(small1)batch1 = dict(batch1)batch2 = dict(batch2)batch1, batch2 = choose_batch(orders1, batch1, batch2, keys)return batch1, batch2# 这个是在大的数组中重新选取
def choose_big(big1, orders1, batch, batch2):orders1 = dict(orders1)keys = list(big1)# 批次数组有两种,一种是存储订单,一种是存储货品batch1 = dict(batch)batch2 = dict(batch2)batch1, batch2 = choose_batch(orders1, batch1, batch2, keys)return batch1, batch2# 然后写一个迭代函数
def gen_ite(order, btch_1, btch_2):"""参考遗传算法的精英基因保留的思想,我们在迭代的过程中将批次中订单数较多的保留,这里每次选取10%的批次然后需要将这些批次标记为已经选择然后重新在为选取的订单集划分大小订单选取的批次可以直接新增在下一代的开头:param order: 订单字典,订单号对应相应的货品种类:param btch_1::param btch_2::return: 最终多次迭代后的结果"""btch_1 = dict(btch_1)btch_2 = dict(btch_2)for i0 in range(1):# 先对批次的订单数排序 —— 降序排序btch = sorted(btch_1.items(), key=lambda x: len(x[1]), reverse=True)btch_len = len(btch)index0 = btch_len // 5# 生成两个新的字典,和btch1,2类似bt1 = {}bt2 = {}# 先将精英保留# 选择字典choose_0 = {}for j in order.keys():choose_0[j] = 0for k in range(index0):for o0 in btch_1[k][1]:choose_0[o0] = 1bt1[k] = btch_1[btch[k][0]]bt2[k] = btch_2[btch[k][0]]keys = []for m in order.keys():if choose[m] == 0:keys.append(m)# # 接下来划分大小订单# big1, small1 = big_and_small(choose, orders)# print('big: %d small: %d' % (len(big1), len(small1)))# # 之后根据划分的大小订单在得到新的分批# bt1, bt2 = choose_big(big1, orders, bt1, bt2)# print(len(bt1), len(bt2))# # bt1, bt2 = choose_small(orders, bt1, bt2, small1)# # print(len(bt1), len(bt2))bt1, bt2 = choose_batch(order, bt1, bt2, keys)# 然后比较新旧分批的批数来更新批次if len(bt1) < len(btch_1):btch_1 = bt1btch_2 = bt2print('第%d次, 批次数为%d' % (i0 + 1, len(btch_1)))le = 0for bi in btch_1:le += len(btch_1[bi])print(le)return btch_1, btch_2# 写一个计算距离的函数
def compute_dis(order, batch10, goods):""":param order: 其实就是订单集合,通过订单可以知道需要的货品:param batch10: 批次下的订单列表:param goods: 当前批次下的货物关于货物列表的索引:return: 返回距离和"""order = dict(order)batch10 = list(batch10)goods = dict(goods)dis = 0for bat in batch10:index_max = -1index_min = -1for good in order[bat]:if index_min == -1 and index_max == -1:index_max = index_min = goods[good]else:index_max = max(index_max, goods[good])index_min = min(index_min, goods[good])dis += index_max - index_minreturn dis# 写一个第二问的解决函数
def solution2(order, batch2, batch1):batch1 = dict(batch1)batch2 = dict(batch2)order = dict(order)pre_total = 0ans = []p = 0total = 0start0 = time.time()for batch in batch2.items():# 现在是对每一个批次# 先建立货物种类到下标映射, 和下标到货物的映射good_to_index = {}index_to_good = batch[1]for index1 in range(len(index_to_good)):good = index_to_good[index1]good_to_index[good] = index1dis = compute_dis(order, batch1[batch[0]], good_to_index)print('当前第%d批原始距离为 %d' % (p + 1, dis))pre_total += disfor i0 in range(200000):j = random.randint(0, len(index_to_good) - 1)k = random.randint(0, len(index_to_good) - 1)if j == k:continue# 获取货物信息good1, good2 = index_to_good[j], index_to_good[k]# 先交换temp = good_to_index[good1]good_to_index[good1] = good_to_index[good2]good_to_index[good2] = tempif dis > compute_dis(order, batch1[batch[0]], good_to_index):index_to_good[j], index_to_good[k] = good2, good1dis = compute_dis(order, batch1[batch[0]], good_to_index)else:good_to_index[good1], good_to_index[good2] = good_to_index[good2], good_to_index[good1]print('迭代后 第%d批原始距离为 %d' % (p + 1, dis))total += disans.append(index_to_good)p += 1print('原始距离', pre_total)print('最终距离', total)end0 = time.time()print('时间消耗', end0 - start0)return ans# 找到订单的起始点
def find_min_and_max_index(order_good, good_index):order_good = list(order_good)good_index = dict(good_index)min_index = max_index = -1for good_name in order_good:if min_index == -1 and max_index == -1:min_index = max_index = good_index[good_name]continuemin_index = min(min_index, good_index[good_name])max_index = max(max_index, good_index[good_name])return min_index, min_indexdef right_find(order, batch1, good_index, pos):order = dict(order)batch1 = list(batch1)good_index = dict(good_index)ans = ''j = int(pos)tar = jfor name in batch1:i1, i2 = find_min_and_max_index(order[name], good_index)if i1 >= pos and (j > i1 or j == pos):j = i1ans = nametar = i2return ans, tardef left_find(order, batch1, good_index, pos):order = dict(order)batch1 = list(batch1)good_index = dict(good_index)ans = ''j = postar = jfor name in batch1:i1, i2 = find_min_and_max_index(order[name], good_index)if i1 <= pos and (j < i1 or j == pos):j = i2ans = nametar = i1return ans, tardef find_people(distance):"""找工人函数我们根据方向和工人所处的位置决定,选取哪个工人:param distance: 距离数组:return: 返回工人的下标"""# 答案初始化为下标0ans = 0distance = list(distance)for i1 in range(1, len(distance)):if distance[i1] < distance[ans]:ans = i1return ansdef solution3(order, batch1, batch2, n):"""该函数主要用于求解第三问:param order: 订单对应的货品,字典对象:param batch1: 分批好的订单,批号对应的订单:param batch2: 分批好的订单对应的货品种类,批号对应的种类:param n: 工人的数量:return: 返回第三问需要的结果"""# 先将这些字典对象示例化,实现可变参数在函数内和外的分离batch1 = dict(batch1)order = dict(order)batch2 = dict(batch2)# 答案,用列表存储,每一个其实也是列表,分别对应答案需要的答案ans = []peo_dis1 = []            # 用来计算每一批员工的总路程total_dis0 = []          # 总距离for i1 in range(n):total_dis0.append(0)# 我们遍历批次for i1 in batch1.keys():# 实例化批次对应的订单和货品种类,这里其实还是将其变为数组的形式bat_or = list(batch1[i1])bat_go = list(batch2[i1])# 货品在该批次中对应的位置, 即货架的位置good_index = {}# 遍历一遍订单和货品种类,这里的顺序其实就是第二问求解的摆放顺序for ind in range(len(bat_go)):# 先获取当前的货物good = bat_go[ind]# 利用字典的映射关系,设置货物对应的摆放位置的关系good_index[good] = indpeople = []         # 工人列表,用来存储工人的位置task = []           # 任务列表,用来存储工人当前批干的第几个订单peo_dis0 = []       # 当前批的距离# 先将这两个列表都初始化为0for p in range(n):people.append(0)task.append(0)peo_dis0.append(0)# 当批次中的订单还有的时候,需要进行分配while len(bat_or) > 0:# 我们可以先进行正向的寻找, 先找当前最小的下标,同级优先按工人编号寻找p = find_people(peo_dis0)# 获取当前工人的位置pos = people[p]# 然后在以当前工人位置为起点,向右寻找最近的订单,并返回订单号和订单完成时工人的位置name, next_pos = right_find(order, bat_or, good_index, pos)# 如果找不到订单,说明右边没有订单了,需要逆向寻找if name == '':# 向当前位置最左边找离最近的订单name, next_pos = left_find(order, bat_or, good_index, pos)task[p] += 1        # 当前工人的任务数 + 1temp = [name, i1 + 1, p + 1, task[p]]        # 当前行的答案ans.append(temp)        # 将这一行的答案添加到最终答案中bat_or.remove(name)     # 将这个订单从列表中移除print(next_pos)# 计算移动距离dis = abs(pos - next_pos)# 加和peo_dis0[p] += dispeople[p] = next_pospeo_dis1.append(peo_dis0)for ik in range(n):total_dis0[ik] += peo_dis0[ik]# 返回答案return ans, peo_dis1, total_dis0if __name__ == "__main__":start = time.time()# 先写一个文件路径filepath = '附件1:订单信息.csv'# 先读取文件# 使用 pandas 内置的读取方式df = pd.read_csv(filepath)  # 将文件读取为一个dataframe格式con = pd.DataFrame(df, columns=['OrderNo', 'ItemNo'])orders = {}for line in con.index:if not con.iloc[line]['OrderNo'] in orders:orders[con.iloc[line]['OrderNo']] = []orders[con.iloc[line]['OrderNo']].append(con.iloc[line]['ItemNo'])s = set(orders.keys())print(len(s))# 选取字典,表示对应订单是否被选取choose = {}# 初始化为未被选取for key in orders.keys():choose[key] = 0big, small = big_and_small(choose, orders)print(len(big), len(small))b1, b2 = choose_big(big, orders, {}, {})b1, b2 = choose_small(orders, b1, b2, small)end = time.time()print(end - start)print(len(b1), len(b2))btch1, btch2 = dict(b1), dict(b2)file = open('result1.csv', 'w+')result1 = 'OrderNo,GroupNo\n'for b in btch1.keys():for o in btch1[b]:result1 += o + ',' + str(b + 1) + '\n'print(result1, file=file)file.close()ans2 = solution2(orders, batch1=btch1, batch2=btch2)result2 = 'ItemNo,GroupNo,ShelfNo\n'for index in btch2.keys():btch2[index] = ans2[index]for _i in range(len(ans2[index])):result2 += ans2[index][_i] + ',' + str(index + 1) + ',' + str(_i + 1) + '\n'file = open('result2.csv', 'w+')print(result2, file=file)file.close()ans3, peo_dis, total_dis = solution3(orders, batch2=btch2, batch1=btch1, n=5)result3 = 'OrderNo,GroupNo,WorkerNo,TaskNo\n'for a in ans3:for index in range(len(a)):result3 += str(a[index])if index + 1 < len(a):result3 += ','else:result3 += '\n'file = open('result3.csv', 'w+')print(result3, file=file)file.close()result4 = ''for b in peo_dis:for index in range(len(b)):result4 += str(b[index])if index + 1 < len(b):result4 += ','else:result4 += '\n'file = open('员工路程,单批.csv', 'w+')print(result4, file=file)file.close()file = open('员工路径总和.csv', 'w+')result4 = ''for i in range(len(total_dis)):result4 += str(total_dis[i])if i + 1 < len(total_dis):result4 += ','else:result4 += '\n'print(result4, file=file)file.close()

总结

这次其实可以优化的地方蛮多的,比如第一问其实可以使用遗传算法进行优秀批次的保留,然后不断筛选(可是我在实践编程中,出了bug没有实现成功)。

华中杯 数学建模 A题简单复盘(附Python源码)相关推荐

  1. 2022年第十四届华中杯数学建模A题解题思路附代码

    A 题 分拣系统优化问题         某电商公司配送中心的工作流程分为统计汇总.转运上架.按订单分拣.核对打包等 步骤.其中,分拣环节操作复杂,耗时较长,其效率是影响配送中心整体性能的关键因素. ...

  2. 2023年华中杯数学建模B题小学数学应用题相似性度量及难度评估解题全过程文档及程序

    2023年华中杯数学建模 B题 小学数学应用题相似性度量及难度评估 原题再现   某 MOOC 在线教育平台希望能够进行个性化教学,实现用户自主学习.在用户学习时,系统从题库中随机抽取若干道与例题同步 ...

  3. 2023年华中杯数学建模A题新型镇静药物临床实验疗效分析与预测解题全过程文档及程序

    2023年华中杯数学建模 A题 新型镇静药物临床实验疗效分析与预测 原题再现   临床研究是新药物研究中的关键环节.本题拟围绕一种新型镇静药物的临床实验数据分析展开. 尝试根据附件中提供的数据和相关材 ...

  4. 2023年第十五届华中杯数学建模赛题浅析

    2023年五一假期期间,数学建模竞赛就有四场,各种比赛各种需求应接不暇.因此,对于本次浅析有不足的地方欢迎大家指出.为了更好的帮助大家华中杯选题,首先为大家带来ABC三个题目的浅析,帮助大家更快的选题 ...

  5. 2023华中杯数学建模C题完整模型代码

     已完成全部模型代码,文末获取. 摘要 随着工业化和城市化的快速发展,空气污染已经成为全球性的环境问题.细颗粒物(PM2.5)等污染物对人类健康.生态环境和社会经济造成了严重影响.本研究旨在深入探究影 ...

  6. 2021年华中杯数学建模挑战赛B题技术问答社区重复问题识别求解全过程文档及程序

    2021年华中杯数学建模 B题 技术问答社区重复问题识别 原题再现:   技术社区问答平台作为用户互相分享交流的社区平台,近年来逐步成为用户寻找技术类疑难解答的首要渠道. 各分类技术性问题的文本数据量 ...

  7. 2022年华中杯数学建模挑战赛A题分拣系统优化问题求解全过程文档及程序

    2022年华中杯数学建模 A 题 分拣系统优化问题 原题再现:   某电商公司配送中心的工作流程分为统计汇总.转运上架.按订单分拣.核对打包等步骤.其中,分拣环节操作复杂,耗时较长,其效率是影响配送中 ...

  8. 【2022年华为杯数学建模E题赛后总结加思路详细介绍配代码----10月11号写的总结】

    提示:下文将介绍2022年华为杯数学建模E题赛后总结加思路详细介绍配代码 傻逼队友,傻逼队友,傻逼队友一定要看好人在进行组队,这是劝告. 这里有几点总结进行描述: 第一,图一定要尽量多,对图的解释要多 ...

  9. 2022华数杯数学建模 A题B题C题 思路模型资料汇总

    2022年第三届华数杯数学建模 A题B题C题 思路模型资料汇总 本次比赛将提供各题思路模型代码等全套资料,已发布往届赛题与优秀获奖论文 看我个人主页自取上述全部资料: [个人主页] 一.赛题分析 (赛 ...

最新文章

  1. 机器学习虽好,也要看什么场合!
  2. Leetcode上的题目
  3. Mac os x下配置nginx + php
  4. DIV层+CSS实现锁屏
  5. [动态代理三部曲:上] - 动态代理是如何坑掉了我4500块钱
  6. canal mysql 数据同步
  7. docker版本包 乌班图_Ubuntu安装Docker方式一:软件包安装
  8. [html] 写一个水平竖直居中的弹窗,带遮罩层的布局
  9. 博客搬家到 http://fresky.github.io/ - Visual Studio的插件Pdbproj可以把pdb转换成C++项目...
  10. 鲲鹏性能优化十板斧(四)——磁盘IO子系统性能调优
  11. PAT-Travel Plan (30)-Dijkstra和SPFA
  12. windows-API劫持(API-HOOK)
  13. 操作系统实验1—基于优先数的时间片轮转调度算法调度处理模拟程序设计
  14. 聊一聊TVS管的电路使用
  15. 百度tts语音合成使用
  16. word无法读取此文件,文档可能已损坏_可能的补救方法
  17. 【阿里巴巴Java编程规范学习 五】MySQL数据库规约
  18. 轻松获取Excel安装路径
  19. 由交通银行卡被盗42万元谈网络应用安全攻防
  20. Oracle获取一周前,一个月前,一年前的日期

热门文章

  1. 一起来读《游戏设计梦工厂》
  2. Twitter推荐算法总架构和算法说明
  3. 简单了解java连接数据库的操作步骤
  4. MYSQL工具mysqlhotcopy使用
  5. MFC/WIN32程序美化之使用skinH更换皮肤
  6. python输入123输出321_python逆序输出,编程从键盘输入一个三位数,将它们逆序输出。如输入123,输出321.谢谢...
  7. 内部赛nbsp;3nbsp;Dnbsp;Delinbsp;Deli
  8. linux 符号连接文件,Linux 硬链接和软链接(符号链接)
  9. 【SCL】博图SCL语言学习 (指令篇 )
  10. sed的用法与实例介绍