今天想和大家分享一个问题的解决办法,这个问题是自己在项目开发的过程中遇到的。经过思考和对资料的查找,最终想出了该问题的解法,趁着周末有点时间就把它整理并分享出来。
在描述问题之前,需要先了解涉及到的名词概念,便于对后续内容的阅读。
名称解释
- sku(仓储相关概念):Stock Keeping Unit 的首字母缩写,指的是一个属性确定的商品,如衣服其有两个属性,颜色和尺寸。那么一件黄色的 X 码的衣服与一件红色的 X 码的衣服就是不同的 sku
- 分播槽(仓储相关概念):一个槽口,用于存放从仓库中拣取出来的商品
- 集合的基数(集合相关概念):集合元素的个数称之为集合的基数,如集合A有6个元素,那么可以将集合A的基数记为card(A) = 6
- 二部图(图论相关概念):无向图的一种特殊模型,如果顶点 V 可分割为两个互不相交的子集( A , B ),并且图中的每条边( i ,j )所关联的两个顶点 i 和 j 分别属于这两个不同的顶点集(i in A , j in B ),则称图 G 为一个二分图。通俗点说,就是如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图
- 节点的度:对于无向图而言,指和该节点相关联的边的条数,又称关联度
有了以上概念的相关理解之后,我们理解相关问题便会容易很多。下面我们来看相关问题描述。
问题描述
在系统运行的过程中,每个分播槽都只能绑定一种 sku 进行分播。上游系统会下发一系列订单到业务系统中进行执行,其中每个订单都关联着一系列的 sku,对于上游系统下发的订单,业务系统如果因为当前可用分播槽数不足,无法让每个订单都执行到,那么就需要选择一部分订单进行执行,同时回退一部分订单给上游系统。问题就在于,如何从这上游系统下发的订单集合中得到满足条件的元最大的订单子集。
为加深对问题的理解,给出如下两个示例及其相关解释:
示例一:
订单及其关联的sku:
order1:sku1,sku3,sku5,sku6
order2:sku1,sku2,sku4,sku8
order3:sku2,sku4,sku8
order4:sku1,sku2,sku3,sku4,sku5,sku6
order5:sku1,sku3
绑定了sku的分播槽:
分播槽1:sku1
分播槽2:sku3
未绑定sku的分播槽口数: 3
返回:{order2,order3,order5}
解释:order1 关联的 sku 需要新绑定 2 个分播槽位( sku5 , sku6 ),order2 和 order3 需要新绑定 3 个分播槽位( sku2 , sku4 , sku8 ),但是 3 个分播槽绑定了 sku2 , sku4 , sku8 之后,order2 和 order3 都可以满足,如果绑定了 sku5 , sku6 的话,就只有 order1 才能执行。对于 order4,其关联的 sku 需要新绑定 4 个分播槽位( sku2 , sku4 , sku5 , sku6 ),而未绑定 sku 的分播槽口数只有 3个,不能满足其要求,为此,不对订单 order4 进行考虑,而订单 order5 其不需要新绑定分播槽位,就可以直接执行
示例二:
订单及其关联的sku:
order1:sku1,sku3
order2:sku1,sku4
order3:sku1,sku6
order4:sku2,sku5
order5:sku2,sku5
绑定了sku的分播槽: 无
未绑定sku的分播槽口数: 2
返回:{order4,order5}
解释:对于每个订单,其都需要新绑定 2 个分播槽位,选择了 order1,order2,order3 中的任意一个订单,则其都不能同时选择其它订单。而 order4,order5 所需拣取的sku是相同的,为此,order4 与 order5 可以同时选择
解法
将订单和 sku 看成一个个的节点,如果订单与 sku 之间有关联,那么就将订单节点和 sku 节点用一条边连接起来,这就形成了一个无向图。由于订单与订单,sku 与 sku 之间并没有关联,为此,无向图的节点可以划分为 2 个子集,一个节点子集是订单,一个节点子集是 sku。下面我们以示例一为例,来讲解该题的相关解法,对示例一中的例子,我们可以得到如下的二部图:
由于有些订单所关联的 sku 已经绑定了分播槽位,对于此类已经绑定了分播槽位的 sku 我们可以不对其进行考虑,为此,我们可以对二部图进行一些预处理。消除掉那些已经绑定了分播槽的 sku 节点及其所关联的边。我们可以得到如下的二部图:
我们再度观察二部图及其示例相关解释,可以发现对于度超过未绑定 sku 的分播槽口数的订单节点,其是一定无法被选择的,为此,我们可以过滤掉那些订单节点。我们可以得到如下的二部图:
在过滤完“不可能”订单节点之后,我们还需要过滤掉那些“不需要处理”便可直接选择的订单(当然,我们需要把这部分订单作为结果的一部分进行返回)。
在经过数据的预处理之后,我们得到的订单以及 sku 便是需要选取的,且其是符合条件约束的。
对订单的选取,我们可以转化为对 sku 的选取问题,也就转化成了选择“未绑定槽口”数的 sku 组成的集合,使得订单中关联的 sku 是所选 sku 组成集合的子集,这么一个问题。
于是,我们可以遍历所有“未绑定槽口”数目的 sku 组成的所有集合,并得到对应所选取 sku 集合中满足条件的订单数目最多的那个 sku 的集合。
对于生成所有 sku 固定元素的集合列表的方法,我们可以采用递归的方式进行实现,定义方法 f(n,k) 为n个元素所组成的集合中选取k个元素的所构成的集合,则 f(n,k) = j + f(s,k-1),k>0 , 其中 j 表示从 n 中选取的任意一个元素,s 表示去除掉元素 j 之后的集合。其代码如下:
其选单过程核心代码如下:
时间复杂度分析: 生成所有固定数目的sku集合的时间复杂度为 \({n \choose k}\),其中 n 为 sku 的数量,k 为未绑定槽口数。由于对每个 sku 集合,都需要遍历一遍各订单节点及其关联 sku 。为此,该算法的时间复杂度为 \(mk{n \choose k}\),其中m为订单节点的数量
后话
对于上面所实现的选单算法,其是基于所有订单在执行过程中订单相关的 sku 绑定分播槽后就不再改变这一前提下实现的。当我们抛开这个约束条件并且在订单执行的过程中,当所有订单相同的 sku 都拣取之后能够对分播槽进行解绑时,理论上来说,我们能够执行的订单数目就会变成无限,但是这样就会涉及到仓储机器人对料箱的搬运效率(因为对料箱搬运顺序有要求)以及缓存货架(机器人搬运出来的料箱暂存的地方)限制的问题。当考虑到该层面时,就是整个链路的优化问题了,为此暂时不对其做考虑。
这个是本人的公众号,致力于写出绝大部分人都能读懂的技术文章。欢迎相互交流,我们博采众长,共同进步。