【问题标题】:Efficiently find every combination of assigning smaller bins to larger bins有效地找到将较小的 bin 分配给较大的 bin 的每种组合
【发布时间】:2015-11-22 12:57:18
【问题描述】:

假设我有 7 个小箱子,每个箱子里有以下数量的弹珠:

var smallBins = [1, 5, 10, 20, 30, 4, 10];

我将这些小箱子分配给 2 个大箱子,每个大箱子的最大容量如下:

var largeBins = [40, 50];

我想找到如何在不超出容量的情况下将小箱子分布在大箱子中的每一种组合(例如,将小箱子 #4、#5 放在大箱子 #2 中,其余放在 #1 中)。

约束:

  • 每个小箱子都必须分配给一个大箱子。
  • 一个大箱子可以空着

这个问题很容易在 O(n^m) O(2^n) 时间内解决(见下文):只需尝试每种组合,如果不超过容量,则保存解决方案。我想要更快的东西,可以处理可变数量的垃圾箱。我可以使用什么晦涩的图论算法来减少搜索空间?

//Brute force
var smallBins = [1, 5, 10, 20, 30, 4, 10];
var largeBins = [40, 50];

function getLegitCombos(smallBins, largeBins) {
  var legitCombos = [];
  var assignmentArr = new Uint32Array(smallBins.length);
  var i = smallBins.length-1;
  while (true) {
    var isValid = validate(assignmentArr, smallBins, largeBins);
    if (isValid) legitCombos.push(new Uint32Array(assignmentArr));
    var allDone = increment(assignmentArr, largeBins.length,i);
    if (allDone === true) break;
  }
  return legitCombos;
}

function increment(assignmentArr, max, i) {
  while (i >= 0) {
    if (++assignmentArr[i] >= max) {
      assignmentArr[i] = 0;
      i--;
    } else {
      return i;
    }
  }
  return true;
}

function validate(assignmentArr, smallBins, largeBins) {
  var totals = new Uint32Array(largeBins.length);
  for (var i = 0; i < smallBins.length; i++) {
    var assignedBin = assignmentArr[i];
    totals[assignedBin] += smallBins[i];
    if (totals[assignedBin] > largeBins[assignedBin]) {
      return false;
    }
  }
  return true;
}
getLegitCombos(smallBins, largeBins);

【问题讨论】:

  • 你确定你的代码是 O(n^m) 吗?因为每个都存在 2^n 组合,在最坏的情况下,算法需要将它们全部返回。
  • 我可能搞错了...检查我的推理:每个小箱子 m 可以分配给每个大箱子 n,总共 n^m。
  • 你听说过所谓的bin packing problem吗?祝你好运找到更有效的解决方案 - 你可以赢得很多钱 :-)
  • 谢天谢地,这比装箱问题要简单得多。我可以使用一些相同的原则(例如,排序箱从大到小,如果超过容量,跳过尝试将较小的值添加到它)但我认为存在一些将它击倒一个数量级的东西,有点像分配问题的匈牙利算法。
  • 对不起,马特,确实是 O(2^n) 只是为了找到其中一个大垃圾箱的解决方案(即使您将另一个留空)。它实际上比装箱更难,而不是更容易。基本上你分别解决两个大箱子的问题(将另一个箱子视为第二个约束给定的空),然后你可以在O(n*(a + b)) = O(2^N) 中组合结果,其中ab 是每个组合的数量两个大垃圾箱。

标签: javascript algorithm math graph-theory mathematical-optimization


【解决方案1】:

这是我的繁琐递归尝试,以避免重复并从太大的金额中提前退出。该函数假定重复元素以及 bin 大小在输入中分组和计数。不是将每个元素放置在每个 bin 中,而是将每个元素仅放置在一个重复的 bin 中;并且每个具有重复项的元素都被明确分区。

例如,在我的结果中,[[[1,10,20]],[[4,5,10,30]]] 的组合出现了一次;而在 Leo 的回答中的 SAS 示例中,两次:一次为 IN[1]={1,3,4} IN[2]={2,5,6,7},另一次为 IN[1]={1,4,7} IN[2]={2,3,5,6}

但是,不能保证效率或平稳运行,因为它几乎没有经过测试。也许堆叠调用而不是递归可以减轻浏览器的负担。

JavaScript 代码:

function f (as,bs){

  // i is the current element index, c its count; 
  // l is the lower-bound index of partitioned element
  function _f(i,c,l,sums,res){

    for (var j=l; j<sums.length; j++){
      // find next available duplicate bin to place the element in
      var k=0;
      while (sums[j][k] + as[i][0] > bs[j][0]){
        k++;
      }

      // a place for the element was found          
      if (sums[j][k] !== undefined){
        var temp = JSON.stringify(sums),
            _sums = JSON.parse(temp);
        _sums[j][k] += as[i][0];

        temp = JSON.stringify(res);           
        var _res = JSON.parse(temp);

        _res[j][k].push(as[i][0]);

        // all elements were placed
        if (i == as.length - 1 && c == 1){
          result.push(_res);
          return;

        // duplicate elements were partitioned, continue to next element
        } else if (c == 1){
          _f(i + 1,as[i + 1][1],0,_sums,_res);

        // otherwise, continue partitioning the same element with duplicates
        } else {
          _f(i,c - 1,j,_sums,_res);
        }
      }
    }
  }

  // initiate variables for the recursion 
  var sums = [],
      res = []
      result = [];

  for (var i=0; i<bs.length; i++){
    sums[i] = [];
    res[i] = [];
    for (var j=0; j<bs[i][1]; j++){
      sums[i][j] = 0;
      res[i][j] = [];
    }  
  }

  _f(0,as[0][1],0,sums,res);
  return result;
}

输出:

console.log(JSON.stringify(f([[1,1],[4,1],[5,1],[10,2],[20,1],[30,1]], [[40,1],[50,1]])));

/*
[[[[1,4,5,10,10]],[[20,30]]],[[[1,4,5,10,20]],[[10,30]]],[[[1,4,5,20]],[[10,10,30]]]
,[[[1,4,5,30]],[[10,10,20]]],[[[1,4,10,20]],[[5,10,30]]],[[[1,4,30]],[[5,10,10,20]]]
,[[[1,5,10,20]],[[4,10,30]]],[[[1,5,30]],[[4,10,10,20]]],[[[1,10,20]],[[4,5,10,30]]]
,[[[1,30]],[[4,5,10,10,20]]],[[[4,5,10,20]],[[1,10,30]]],[[[4,5,30]],[[1,10,10,20]]]
,[[[4,10,20]],[[1,5,10,30]]],[[[4,30]],[[1,5,10,10,20]]],[[[5,10,20]],[[1,4,10,30]]]
,[[[5,30]],[[1,4,10,10,20]]],[[[10,10,20]],[[1,4,5,30]]],[[[10,20]],[[1,4,5,10,30]]]
,[[[10,30]],[[1,4,5,10,20]]],[[[30]],[[1,4,5,10,10,20]]]]
*/

console.log(JSON.stringify(f([[1,1],[4,1],[5,1],[10,2],[20,1],[30,1]], [[20,2],[50,1]])));

/*
[[[[1,4,5,10],[10]],[[20,30]]],[[[1,4,5,10],[20]],[[10,30]]],[[[1,4,5],[20]],[[10,10,30]]]
,[[[1,4,10],[20]],[[5,10,30]]],[[[1,5,10],[20]],[[4,10,30]]],[[[1,10],[20]],[[4,5,10,30]]]
,[[[4,5,10],[20]],[[1,10,30]]],[[[4,10],[20]],[[1,5,10,30]]],[[[5,10],[20]],[[1,4,10,30]]]
,[[[10,10],[20]],[[1,4,5,30]]],[[[10],[20]],[[1,4,5,10,30]]]]
*/

这是第二个更简单的版本,它仅在无法放置元素时尝试终止线程:

function f (as,bs){

  var stack = [],
      sums = [],
      res = []
      result = [];

  for (var i=0; i<bs.length; i++){
    res[i] = [];
    sums[i] = 0;
  }

  stack.push([0,sums,res]);

  while (stack[0] !== undefined){
    var params = stack.pop(),
        i = params[0],
        sums = params[1],
        res = params[2];

    for (var j=0; j<sums.length; j++){
      if (sums[j] + as[i] <= bs[j]){
        var _sums = sums.slice();
        _sums[j] += as[i];

        var temp = JSON.stringify(res);
        var _res = JSON.parse(temp);

        _res[j].push(i);

        if (i == as.length - 1){
          result.push(_res);

        } else {
          stack.push([i + 1,_sums,_res]);
        }
      }
    }
  }

  return result;
}

输出:

var r = f([1,5,10,20,30,4,10,3,4,5,1,1,2],[40,50,30]);
console.log(r.length)

console.log(JSON.stringify(f([1,4,5,10,10,20,30], [40,50])));

162137 

[[[30],[1,4,5,10,10,20]],[[10,30],[1,4,5,10,20]],[[10,20],[1,4,5,10,30]]
,[[10,30],[1,4,5,10,20]],[[10,20],[1,4,5,10,30]],[[10,10,20],[1,4,5,30]]
,[[5,30],[1,4,10,10,20]],[[5,10,20],[1,4,10,30]],[[5,10,20],[1,4,10,30]]
,[[4,30],[1,5,10,10,20]],[[4,10,20],[1,5,10,30]],[[4,10,20],[1,5,10,30]]
,[[4,5,30],[1,10,10,20]],[[4,5,10,20],[1,10,30]],[[4,5,10,20],[1,10,30]]
,[[1,30],[4,5,10,10,20]],[[1,10,20],[4,5,10,30]],[[1,10,20],[4,5,10,30]]
,[[1,5,30],[4,10,10,20]],[[1,5,10,20],[4,10,30]],[[1,5,10,20],[4,10,30]]
,[[1,4,30],[5,10,10,20]],[[1,4,10,20],[5,10,30]],[[1,4,10,20],[5,10,30]]
,[[1,4,5,30],[10,10,20]],[[1,4,5,20],[10,10,30]],[[1,4,5,10,20],[10,30]]
,[[1,4,5,10,20],[10,30]],[[1,4,5,10,10],[20,30]]]

【讨论】:

  • 这看起来不错,请给我一点时间来消化它,我会接受它作为答案。
  • 我不知道它是否有效,哈哈。如果你最终测试它,请让我知道是否有问题
  • 对于我的用例,不幸的是它看起来不会起作用。原因是每一小袋弹珠都是独一无二的,所以袋子 A 有 10 个弹珠!== 袋子 B 有 10 个弹珠。
  • @MattK 没有什么可以阻止您将两者都放入我的代码中,例如,以[10,1],[10,1][10,2] 或什至[10,1],[10,1],[10,2] 的任何一种方式进行操作 - 我认为第一个应该适合您的目的描述。您描述的版本可能具有稍微简单的代码,具有较少的分层对象。您是否也想将相同大小的 bin 视为唯一的?
  • 随着样本量的增加,它会停止返回所有结果。例如:smallBins = [[1,1],[5,1],[10,1],[20,1],[30,1],[4,1],[10,1],[3,1],[4,1],[5,1],[1,1],[1,1],[2,1]]; bigBins = [[40,1],[50,1],[30,1]]; 在应该返回 162137 时返回 64387 结果。
【解决方案2】:

这个问题经常出现,以至于大多数约束逻辑编程系统都包含一个谓词来显式地建模它。在 OPTMODEL 和 CLP 中,我们称之为pack

proc optmodel;
    set SMALL init 1 .. 7, LARGE init 1 .. 2;
    num size    {SMALL} init [1 5 10 20 30 4 10];
    num capacity{LARGE} init [40 50];

    var WhichBin {i in SMALL} integer >= 1 <= card(LARGE);
    var SpaceUsed{i in LARGE} integer >= 0 <= capacity[i];

    con pack( WhichBin, size, SpaceUsed );

    solve with clp / findall;

    num soli;
    set IN{li in LARGE} = {si in SMALL: WhichBin[si].sol[soli] = li}; 
    do soli = 1 .. _nsol_;
        put IN[*]=;
    end;
quit;

此代码在我的笔记本电脑上在 0.06 秒内生成所有解决方案:

IN[1]={1,2,3,4,6} IN[2]={5,7}
IN[1]={1,2,3,4} IN[2]={5,6,7}
IN[1]={1,2,3,6,7} IN[2]={4,5}
IN[1]={1,2,5,6} IN[2]={3,4,7}
IN[1]={1,2,5} IN[2]={3,4,6,7}
IN[1]={1,2,4,6,7} IN[2]={3,5}
IN[1]={1,2,4,7} IN[2]={3,5,6}
IN[1]={1,2,4,6} IN[2]={3,5,7}
IN[1]={1,3,4,6} IN[2]={2,5,7}
IN[1]={1,3,4} IN[2]={2,5,6,7}
IN[1]={1,5,6} IN[2]={2,3,4,7}
IN[1]={1,5} IN[2]={2,3,4,6,7}
IN[1]={1,4,6,7} IN[2]={2,3,5}
IN[1]={1,4,7} IN[2]={2,3,5,6}
IN[1]={2,3,4,6} IN[2]={1,5,7}
IN[1]={2,3,4} IN[2]={1,5,6,7}
IN[1]={2,5,6} IN[2]={1,3,4,7}
IN[1]={2,5} IN[2]={1,3,4,6,7}
IN[1]={2,4,6,7} IN[2]={1,3,5}
IN[1]={2,4,7} IN[2]={1,3,5,6}
IN[1]={3,5} IN[2]={1,2,4,6,7}
IN[1]={3,4,7} IN[2]={1,2,5,6}
IN[1]={3,4,6} IN[2]={1,2,5,7}
IN[1]={3,4} IN[2]={1,2,5,6,7}
IN[1]={5,7} IN[2]={1,2,3,4,6}
IN[1]={5,6} IN[2]={1,2,3,4,7}
IN[1]={5} IN[2]={1,2,3,4,6,7}
IN[1]={4,6,7} IN[2]={1,2,3,5}
IN[1]={4,7} IN[2]={1,2,3,5,6}

只需更改前 3 行即可解决其他实例。但是,正如其他人指出的那样,这个问题是 NP-Hard。所以它可以突然从非常快切换到非常慢。您还可以通过创建一个容量足以容纳整个小物品集合的虚拟大箱子来解决不需要将每个小物品都分配到大箱子的版本。

从手册的“细节”部分可以看出,快速解决实际问题的算法并不简单,它们的实现细节影响很大。我不知道有任何用 Javascript 编写的 CLP 库。您最好的选择可能是将 CLP 包装在 Web 服务中并从您的 Javascript 代码中调用该服务。

【讨论】:

  • 哇,没关系。谢谢指路!你知道这个特定的约束满足问题是否有名字吗?我必须想象有一个可以移植的 C/Java 实现。
  • 名称不同。我称它为可变包装尺寸的装箱。可能有 C 和 Java 中的实现;但是将它们从更广泛的约束满足环境中分离出来可能太难了,不值得。
猜你喜欢
  • 2016-09-14
  • 2020-09-08
  • 1970-01-01
  • 1970-01-01
  • 2011-01-09
  • 1970-01-01
  • 1970-01-01
  • 2019-05-13
  • 1970-01-01
相关资源
最近更新 更多