前言
想起来之前写过一段业务代码来处理库存中的商品。逻辑如下:
仓库中存有多个箱子,每个箱子里面放着一定数量的商品,商品数量不一,现在我要分析出这几箱中商品数量之和最接近目标出库数量的箱子的信息。
本文使用递归实现模拟二进制运算,通过改变对应下标的标志位状态找到所有的组合方式。
该方法并不一定是最优方法,但可能会适合某些场景。
除此之外可以使用 递归+回溯 的方式去处理组合、子集之类的问题。
分析
因为每个箱子都是独立存在的,所以在这个列表里面不存在重复元素一说,分析起来相对简单,
对于有重复元素的添加一个判重方法就可以了。
我只要能够知道所有组合方式,并计算他们与出库数量的差值,获取最小差值的组合即可。
所以问题在于如何获取到所有的组合。
思考
我们可以创建一个和列表同样长度的数组,数组中的数值代表列表中相同下标的元素箱子是否被收纳到当前组合中。
0 表示不包含该箱子 1 表示包含该箱子
为什么要用 0 和 1 ?
根据组合数公式,对于一个存在 n 个不同元素的集合,我们从中任取 m 个元素组成一组的组法有 C nm 种。
而对于全部的组合之和: C n0 + C n1 + C n2 + … + C nn = 2n
在这里我们不考虑空集,所以我们能得到的组合数量是 2n-1
而 n 位长度的二进制能表示的无符号最大值为 2n-1,这两者的数值相同。
而且根据二进制的步进规则,满 2 进 1,除了0、1之外不存在其他的值,符合我们拿来做判断标志的要求。
简单的实例
有列表 :A 【箱子1,箱子2,箱子3】
创建数组 :b 【0,0,0】
模拟二进制加 1 :stepping
stepping
【0,0,0】--------->【1,0,0】 ==》【箱子1】
stepping
【1,0,0】--------->【0,1,0】 ==》【箱子2】
stepping
【0,1,0】--------->【1,1,0】 ==》【箱子1,箱子2】
… …
stepping
【1,0,1】--------->【0,1,1】 ==》【箱子2,箱子3】
stepping
【0,1,1】--------->【1,1,1】 ==》【箱子1,箱子2,箱子3】
完整代码
//商品类
static class Goods {
private static SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
String name;
Integer amount;
Date inWmsTime;
@Override
public String toString() {
return String.format("【商品:%s ,数量:%d ,入库时间:%s】",name,amount,null==inWmsTime?"":sdf.format(inWmsTime));
}
}
public static void main(String[] args) {
//创建六箱商品 商品默认一样 数量不同
List<Goods> goods = new ArrayList<>();
for(int i=0;i<6;i++){
Goods good = new Goods();
good.name = "good";
good.amount = 1+i;
goods.add(good);
}
List<List<Goods>> result = subsets(goods,7);
result.stream().forEach(System.out::println);
}
/**
*
* @param
* @return
*/
public static List<List<Goods>> subsets(List<Goods> goods,Integer target) {
List<List<Goods>> result =new ArrayList<>();
int[] tmparr = new int[goods.size()];
//最小差值
Integer minDiffer = 0;
//判断是否可以继续便利
List<Goods> inner = new ArrayList<>();
while(isContinue(tmparr)){
//步进+1
stepping(0,tmparr);
inner.clear();
//取出集合对应数组相同下标 值为 1 的元素,加入到组合 inner中
for (int i =0 ;i<tmparr.length;i++) {
if(tmparr[i] == 1){
inner.add(goods.get(i));
}
}
//计算当前组合商品总数
Integer currentAmount = inner.stream().map(e->e.amount).reduce(0,Integer::sum);
//计算差值
Integer currentDiffer = currentAmount - target;
//获取不少于目标的最小值
if(currentDiffer >= 0 && currentDiffer <= minDiffer)
result.add(new ArrayList<>(inner));
}
return result;
}
/**
* 模拟二进制
* 例如:
* candidates {1,2,3,4,5}
* a {0,0,0,0,0}
* 从下标第 0 位 + 1, 大于 1时,当前位置为 0 ,下一位 + 1
* =>{1,0,0,0,0}
* =>{0,1,0,0,0}
* =>{1,1,0,0,0}
* =>{0,0,1,0,0}
* ...
* =>{1,1,1,1,1}
* @param index
* @return
*/
private static void stepping(int index,int[] tmparr) {
if (tmparr[index] == 0) {
tmparr[index] = 1;
} else {
if (index < tmparr.length)
tmparr[index] = 0;
stepping(index + 1,tmparr);
}
}
/**
* 判断是否已经遍历到全部组合
* 有 0 存在即没有遍历到全部 接续遍历
* @return
*/
private static boolean isContinue(int[] tmparr) {
for (int i : tmparr) {
if (i == 0 )
return true;
}
return false;
}