最近在做大促需求的时候,根据与产品讨论出来的一个优化方案涉及到两个个人感觉很不错的算法知识,可以简单的抽象成数据结构算法问题。特此分享一下此次实战中的一些思路。
前言:一个拣货任务是仓库内分配作业人员的最小单位,即一个作业人员通过手持RF设备即可获取一个拣货任务,这个拣货任务包含多个商品,也就是一个拣货单包括多个拣货明细。这个明细主要包含信息为库位,需要拣的数量,商品名称。
概念介绍:
- 拣货任务
- 动线排序
一个作业人员获取一个任务,然后根据这个拣货任务的明细进行一个一个拣货,完成拣货任务,如何达到最佳的拣货路线,我们会根据根据路线进行一个排序,库位是有编号的,这个编号可以简单理解成是物理顺序排列的,我们系统生成的时候也要根据这个顺序排好序这样可以让作业人员顺序走动,避免来回走动。
- 拣货容器
其实这次要解决的一个问题是一个容器装箱问题,作业人员会拿一个容器去进行拣货,系统现状是一个拣货任务就拿一个容器,这样存在一个问题,一个拣货单或许装不满,这次就是要提升装载率。 - 考虑到改动成本,我们把拣货单控制成一个拣货单只包含一个明细,这样其实就是一个拣货单一个货位一个商品的维度了,然后根据每个拣货单的体积(即每个库位商品要拣的体积)来计算需要的容器是否装满。最后根据条件,把尽量连续装满的拣货单合并在一个分组里。(这样能够一起拣货装在一个容器)
- 抽象成这样一个问题,连续的库位的拣货单,如果体积合在一起就成一个组,超过为单独一个组。
假设体积为100~110都算装满,这样的一个排序。哪些能组成一个分组呢。
根据算法:
首先看20,不够加入一个虚拟的暂存区
到70加暂存区合计90仍然不满
30合计暂存区超过110,丢弃暂存区头。注意这里为什么不是整个丢弃暂存区,就是如果是那样,就会丢失70+30这次组包成功,这是符合预期的值。
不难得出最后能组成一组的值是:70+30+40.
- 这里存在另一个概念,是否甩尾,所谓的尾巴就是容器内空闲的体积,甩尾即是装满的意思,不甩尾就是不装满。我们的算法设计是支持是否甩尾的。所以如果保证组出来的一定是装满的,就只能得出上面一组结果,剩余的拣货任务,在一定时间内无法装满也需要强制走掉,就是允许不甩尾去拣货,这样上面同样的条件就可以组成。当然,并不是不满就一组,应该尽量让其装满,其思路为:
20不够,放入暂存区
70+暂存区20合计90,不满
90+30超过容器,90组成一个分组,30加入暂存区
…
合计出来的结果分组为 20+70 30+40+40 80
Talk is cheap , show the code
private List<List<PickUnitDTO>> combineContainerByVolume(Long warehouseId, List<PickUnitDTO> taskList, ContainerTypeDTO containerTypeDTO, Boolean isWithoutTail) {
List<List<PickUnitDTO>> pickTaskModelList = Lists.newArrayList();
BizCheck.checkArgument(containerTypeDTO != null && containerTypeDTO.getVolume() != null && containerTypeDTO.getVolumeRate() != null, "volume or rate is not config");
AppSwitcher.switchLog("group volume before %s, volume %s", JSONObject.toJSONString(taskList), String.valueOf(containerTypeDTO));
List<PickUnitDTO> tailPickTaskModelList = Lists.newArrayList();
for (int i = 0; i < taskList.size(); i++) {
PickUnitDTO currentPickUnit = taskList.get(i);
if (isContainerOver(currentPickUnit.getVolume(), containerTypeDTO)) {
// 单个体积超过
List<PickUnitDTO> pickUnitDTOList = Lists.newArrayList();
pickUnitDTOList.add(currentPickUnit);
// 当前拣货单为一组
pickTaskModelList.add(pickUnitDTOList);
if (isWithoutTail) {
// 甩尾
tailPickTaskModelList.clear();
} else {
// 不甩尾
if (CollectionUtils.isNotEmpty(tailPickTaskModelList)) {
// 暂存区为一个分组
pickTaskModelList.add(tailPickTaskModelList);
// 清空暂存
tailPickTaskModelList = Lists.newArrayList();
}
}
continue;
}
/**
* 合计暂存体积与当前拣货单
*/
double volume = currentPickUnit.getVolume().doubleValue();
for (PickUnitDTO pickUnitDTO : tailPickTaskModelList) {
volume += pickUnitDTO.getVolume().doubleValue();
}
if (matchOfContainer(BigDecimal.valueOf(volume), containerTypeDTO)) {
List<PickUnitDTO> pickUnitDTOList = Lists.newArrayList();
pickUnitDTOList.add(currentPickUnit);
if (CollectionUtils.isNotEmpty(tailPickTaskModelList)) {
pickUnitDTOList.addAll(tailPickTaskModelList);
}
tailPickTaskModelList.clear();
pickTaskModelList.add(pickUnitDTOList);
} else if (isContainerOver(BigDecimal.valueOf(volume), containerTypeDTO)) {
if (!isWithoutTail) {
List<PickUnitDTO> pickUnitDTOList = Lists.newArrayList();
pickUnitDTOList.addAll(tailPickTaskModelList);
pickTaskModelList.add(pickUnitDTOList);
tailPickTaskModelList.clear();
tailPickTaskModelList.add(currentPickUnit);
} else {
// tailPickTaskModelList.remove(0);
tailPickTaskModelList.add(currentPickUnit);
for (int j = 0; j < tailPickTaskModelList.size(); j++) {
double sum = tailPickTaskModelList.subList(j, tailPickTaskModelList.size()).stream().mapToDouble(task -> task.getVolume().doubleValue()).sum();
if (matchOfContainer(BigDecimal.valueOf(sum), containerTypeDTO)) {
List<PickUnitDTO> pickUnitDTOList = Lists.newArrayList();
pickUnitDTOList.addAll(tailPickTaskModelList.subList(j, tailPickTaskModelList.size()));
pickTaskModelList.add(pickUnitDTOList);
tailPickTaskModelList = Lists.newArrayList();
}
}
}
} else {
// shortage
tailPickTaskModelList.add(currentPickUnit);
}
}
if (CollectionUtils.isNotEmpty(tailPickTaskModelList)) {
double volume = tailPickTaskModelList.stream().mapToDouble(task -> task.getVolume().doubleValue()).sum();
if (shortOfContainer(BigDecimal.valueOf(volume), containerTypeDTO)) {
if (!isWithoutTail) {
List<PickUnitDTO> pickUnitDTOList = Lists.newArrayList();
pickUnitDTOList.addAll(tailPickTaskModelList);
pickTaskModelList.add(pickUnitDTOList);
}
}
}
AppSwitcher.switchLog("cal val result %s", JSONObject.toJSONString(pickTaskModelList));
return pickTaskModelList;
}
算法2
由于拣货单的顺序之间关系到作业人员的作业顺序,因此根据动线排序完成后,会发现同库位同商品早生成的拣货单可能后作业,因此这里就产生另一个问题,在同样库位商品情况下进行局部排序。
例如这样的情况,需要对5-3-4进行排序。抽象为数组的局部排序:
private void extSortByGmt(List<PickTaskDetailModel> pickTaskDetailModelList) {
List<PickTaskDetailModel> sortedPickTaskDetailList = Lists.newArrayList();
List<PickTaskDetailModel> sameCabinetIdPointerPickTaskDetailList = Lists.newArrayList();
for (int i = 0; i < pickTaskDetailModelList.size(); i++) {
// 取出同一库位的拣货单
PickTaskDetailModel currentPickTaskDetail = pickTaskDetailModelList.get(i);
sameCabinetIdPointerPickTaskDetailList.add(currentPickTaskDetail);
while (i + 1 < pickTaskDetailModelList.size() && currentPickTaskDetail.getCabinetId().equals(pickTaskDetailModelList.get(i + 1).getCabinetId())) {
sameCabinetIdPointerPickTaskDetailList.add(pickTaskDetailModelList.get(i + 1));
i ++;
}
if (sameCabinetIdPointerPickTaskDetailList.size() > 1) {
// id排序(与时间一致)
sameCabinetIdPointerPickTaskDetailList.sort((o1, o2) -> {
if (o1.getId() < o2.getId()) {
return -1;
} else if (o1.getId() > o2.getId()) {
return 1;
}
return 0;
});
sortedPickTaskDetailList.addAll(sameCabinetIdPointerPickTaskDetailList);
} else {
sortedPickTaskDetailList.add(currentPickTaskDetail);
}
sameCabinetIdPointerPickTaskDetailList.clear();
}
pickTaskDetailModelList.clear();
pickTaskDetailModelList.addAll(sortedPickTaskDetailList);
}
总结
很多人觉得做业务需求基本就是CURD,其实复杂的业务需求也会涉及很多算法,程序=数据结构+算法是很有道理的,只是我们平时用的复杂算法很多已经被封装成开源库,这不是让我们忘记算法的重要性,而是让我们可以更专注于业务。如果不重视,真的到遇到业务需求上的算法难题就麻烦了。本文的提到的算法,例如体积组合的那个问题,我相信还有更好的解决方案,例如如果可以跨分区组为一个分组,那问题将变得异常复杂,这里更涉及到动线与体积的优先级问题,在两者之间做个权衡并达到效率最高,这可能已经超过技术侧重思考的问题,对于技术人员已经超纲。但是这可能也是作为技术人员反推业务去思考的点。