【问题标题】:Stacking boxes into fewest number of stacks efficiently?有效地将盒子堆叠成最少数量的堆叠?
【发布时间】:2016-10-24 16:31:15
【问题描述】:

我在一次在线编码挑战中得到了这个问题。

给定一个长度和宽度为 (l, h) 的盒子列表,输出包含所有盒子所需的最小堆叠数,假设您可以将一个盒子堆叠在另一个盒子的顶部,如果它的长度和宽度小于其他盒子的。

我不知道如何提出多项式时间解决方案。我已经构建了一个蛮力解决方案,它递归地创建所有可能的堆栈排列(从 N 个堆栈开始。然后对于每个堆栈,尝试将它放在另一个堆栈的顶部。然后在给定新堆栈排列的情况下递归生成所有堆栈可能性。),然后返回所需的最少堆栈数。

我已经通过一些优化加快了速度:

  • 如果您可以将盒子 A 堆叠在盒子 B 和盒子 C 的顶部,并且可以将盒子 B 堆叠在盒子 C 的顶部,那么请不要考虑将盒子 A 堆叠在盒子 C 上。
  • 记住堆栈排列,仅考虑堆栈的底层和顶层

这是此解决方案的 python 代码:

from time import time

def all_stacks(bottom, top, d={}):

    memo_key = (tuple(bottom), tuple(top))
    if memo_key in d:
        # print "Using MEMO"
        return d[memo_key]

    res = len(bottom)
    found = False
    stack_possibilities = {}
    # try to stack the smallest boxes in all the other boxes
    for j, box1 in enumerate(bottom):
        stack_possibilities[j] = []
        for i, box2 in enumerate(top[j:]):
            # box1 fits in box2
            if fits(box2, box1):
                stack_possibilities[j].append(i + j)
                found = True

    for fr, to_list in stack_possibilities.iteritems():
        indices_to_keep = []
        for box_ind in to_list:
            keep = True
            for box_ind_2 in to_list:
                if fits(top[box_ind], top[box_ind_2]):
                    keep = False
            if keep:
                indices_to_keep.append(box_ind)
        stack_possibilities[fr] = indices_to_keep


    if found:
        lens = []
        for fr, to_list in stack_possibilities.iteritems():
            # print fr
            for to in to_list:
                b = list(bottom)
                t = list(top)
                t[to] = t[fr]
                del b[fr]
                del t[fr]
                lens.append(all_stacks(b, t, d))
        res = min(lens)

    d[memo_key] = res

    return res

def stack_boxes_recursive(boxes):
    boxes = sorted(boxes, key=lambda x: x[0] * x[1], reverse=False)
    stacks = all_stacks(boxes, boxes)
    return stacks

def fits(bigbox, smallbox):
    b1 = smallbox[0] < bigbox[0]
    b2 = smallbox[1] < bigbox[1]
    return b1 and b2


def main():

    n = int(raw_input())
    boxes = []
    for i in range(0,n):
        l, w = raw_input().split()
        boxes.append((long(l), long(w)))
    start = time()
    stacks = stack_boxes_recursive(boxes)
    print time() - start

    print stacks


if __name__ == '__main__':
    main()

输入

17
9 15
64 20
26 46
30 51
74 76
53 20
66 92
90 71
31 93
75 59
24 39
99 40
13 56
95 50
3 82
43 68
2 50

输出:

33.7288980484
6

该算法在几秒钟内 (1-3) 解决了 16 个盒子问题,在大约 30 秒内解决了 17 个盒子问题。我很确定这是指数时间。 (因为堆栈排列的数量是指数级的)。

我很确定有一个多项式时间解,但我不知道它是什么。问题之一是记忆取决于当前的堆栈安排,这意味着您必须进行更多计算。

【问题讨论】:

  • 把它变成图问题。
  • 附带说明:如果您的代码按预期工作并希望使其变得更好,请尝试将其发布在此处:codereview.stackexchange.com
  • 允许旋转盒子吗?
  • 这看起来像一个类似的问题:stackoverflow.com/questions/21712133

标签: python algorithm


【解决方案1】:

让我们构建一个图,其中每个盒子都有顶点,如果 A 可以堆叠在 B 上,则从盒子 A 到盒子 B 有一条边。这个图是非循环的(因为“可以堆叠在顶部”是传递关系和盒装不能堆叠在自身之上)。

现在我们需要找到这个图的最小路径覆盖。这是一个标准问题,它是这样解决的:

  1. 让我们构建一个二分图(原始图中的每个顶点在左侧和右侧都有两个“副本”)。对于原始图中的每条A-&gt;B 边,在A 的左侧副本和B 的右侧副本之间都有一条边。

  2. 答案是框数减去二分图中最大匹配的大小。

二部图为O(n) 顶点和O(n^2) 边。例如,如果我们使用库恩算法来寻找匹配项,总时间复杂度为O(n^3),其中n 是框数。

【讨论】:

  • @AbhishekBansal 一般情况下是 NP 难的。但是,对于任何无环图(如我的回答中所述),它都可以简化为二分图问题,因此在这种特殊情况下它是多项式的。
  • 嗯.. 对我来说看起来不错。 +1
  • @kraskevich 几个问题:
  • 糟糕。没有按Enter的意思。 1. 复制每个节点并使其成为二分法有什么好处? 2.这个问题怎么不是NP难的?
  • @DavidAbrahams 这个想法是答案总是原始图中的顶点数减去最大匹配的大小。它显然在多项式时间内有效。为什么是正确的?直观地说,如果一个盒子与某物匹配,它就站在它上面。可以证明每个匹配确实对应于一堆盒子(粗略地说,没有任何问题,因为原始图是无环的)。
【解决方案2】:

我最近也遇到了这个问题。我提出以下 O(NlogN) 解决方案:

1) 维护一个列表AllStkTops,列出所有当前正在使用的 box-stacks 的最顶层 box。它将被初始化为一个空列表。

2) 按长度递减的顺序对盒子进行排序。 (如果长度相等,则按宽度的递增(是,不递减)顺序对它们进行排序)。

3) 开始挑选箱子(从最长的开始)并将它们放入当前堆栈之一。对于第一个盒子,将没有堆栈,因此它将是第一个盒子堆栈的基础。第二个盒子要么放在第一个盒子的顶部,要么是第二个堆栈的底部。随着我们继续这个过程,我们会意识到对于任何手头的盒子,它的长度将小于或等于所有堆栈中最顶部的盒子。因此,我们只需要担心广度。从第一个堆栈开始检查所有当前堆栈的顶部的宽度,并将其放在堆栈顶部,i) 长度和宽度严格大于当前框的长度和宽度,以及 ii) 最小可能宽度 (为最优)。如果没有一个栈可以容纳这个盒子,创建一个以此盒子为基础的新栈。

请注意,所有堆栈顶部的宽度将按递增顺序排列,因为只有当盒子的宽度大于该时间点的所有堆栈顶部时,我们才会创建一个新堆栈。 (对于等长盒子的情况,我们已经在排序时将它们按宽度递增的顺序排列)。因此,我们可以使用二分搜索过程来找到具有足够大宽度的最窄栈顶。但我们还需要确保它的长度严格大于(而不仅仅是等于)给定框的长度。但是,我声称顶部的长度永远不会成为问题,因为
i) 箱子是按长度递减的顺序挑选的,因此顶部的长度肯定不能严格地减少,并且 ii) 它也可能不相等,因为我们已经按照宽度递增的顺序对长度相等的盒子进行了排序,因此获得前一个盒子的堆栈必须在此堆栈顶部的左侧。

因此,我们可以使用二分搜索过程来找到当前框的最佳栈顶。

我们可以重复上述过程,直到将所有盒子都放入堆栈中。最后,计算 AllStkTops 中的堆栈数。

这是 O(NlogN) 的复杂性,因为排序需要 O(NlogN) 时间,而对每个框的二进制搜索需要 O(logN) 时间。

我很乐意接受有人在我的算法中发现的任何问题和/或缺陷。

干杯!

【讨论】:

    【解决方案3】:

    一开始这看起来很简单,但考虑到各种可能性,很快就变成了一个艰难的过程。然而,我已经成功地在 JS 中实现了我的算法,我对此非常有信心,而且 JS 有一些专长,比如对象是引用类型,在这种特殊情况下,这对我有很大帮助。

    我首先假设我们可以转动盒子,使其长边位于 x 轴上。然后给定的 17 个框在 Chrome 中的 4~10 毫秒之间完成,而在 FF 中则为 15~25 毫秒,因此总共至少 5 个框可以包含所有 17 个。

    此外,我尝试了50 boxes case(每个随机尺寸在 1-100 之间)。因此,50 个盒子的外壳,取决于它们的安装方式,在 Chrome 和 FF 中的分辨率都在难以置信的 250~15000 毫秒之间。我想 70 是这份工作的极限,然后才变得真正无聊。

    代码仍然可以在速度方面得到提升,但到目前为止就是这样。有空我会尽量在一两天内对代码进行详细的描述。

    function insertBoxes(data){
      if (data.length <= 1) return data[0] ? [data] : data;
    
      function flatMap(o){
        return o.inside.length ? o.inside.reduce((p,b) => p.concat(flatMap(b).map(f => f.concat(o.id))),[])
                               : [[o.id]];
      }
    
      function getBest(arr, r = []){
        var i = arr.findIndex(a => a.every(i => !used[i])),len,tgt,bests,best;
        if (i === -1) return r;
          len = arr[i].length;
          tgt = arr.slice(i);
        bests = tgt.filter(a => a.length === len && a.every(x => !used[x]));
         best = bests.map((a,i) => [a.reduceRight((p,c) => p + boxes[c].x + boxes[c].y, 0), i])
                     .reduce((p,c) => c[0] < p[0] ? c : p,[Infinity,-1]);
        bests[best[1]].forEach(i => used[i] = true);
        r.push(bests[best[1]]);
        return getBest(tgt,r);
      }
    
      var boxes = data.map((e,i) => ({id:i, x:Math.max(e[0],e[1]), y:Math.min(e[0],e[1]), inside:[]})),
           used = Array(data.length).fill(false),
        interim = boxes.map((b,_,a) => { a.forEach(box => (b.x > box.x && b.y > box.y) && b.inside.push(box));
                                         return b;
                                       })
                       .map(b => flatMap(b))
                       .reduce((p,c) => p.concat(c))
                       .sort((a,b) => b.length-a.length);
      return getBest(interim).map(b => b.map(i => data[i]))
                             .concat(insertBoxes(data.reduce((p,c,i) => used[i] ? p : (p.push(c),p) ,[])));
    }
    
    var input = [[9, 15], [64, 20], [26, 46], [30, 51], [74, 76], [53, 20], [66, 92], [90, 71], [31, 93], [75, 59], [24, 39], [99, 40], [13, 56], [95, 50], [3, 82], [43, 68], [2, 50]];
       result = [],
           ps = 0,
           pe = 0,
    ps = performance.now();
    result = insertBoxes(input);
    pe = performance.now();
    console.log("boxing", input.length, "boxes done in:", pe-ps,"msecs and all boxes fit in just", result.length,"boxes");
    console.log(JSON.stringify(result));

    注意:上面的代码使用递归的从上到下的方法,但我已经开始认为从下到上的动态编程方法将是这个问题的真正解决方案。

    好的,就像我认为动态编程方法会产生更快的解决方案一样。我没有删除以上内容,请忽略它。

    以下代码将在不到 1 毫秒的时间内解析 17 个项目的盒子数组,在不到 100 毫秒的时间内解析 1000 个项目的盒子数组。

    function boxBoxes(d){
      return d.sort((a,b) => b[0]*b[1] - a[0]*a[1])
              .map(b => b[0] < b[1] ? b : [b[1],b[0]])
              .map(function(b,i,a){
                     b.c = i ? a.slice(0,i)
                                .filter(f => f[0] > b[0] && f[1] > b[1]).length || Infinity
                             : Infinity;
                     return [b];
                   })
              .sort((a,b) => b[0].c - a[0].c)
              .reduceRight(function(p,c,i,a){
                             var r = a.filter(f => f[f.length-1][0] > c[0][0] &&
                                                   f[f.length-1][1] > c[0][1]);
                             r.length && r[0].push(...a.splice(i,1)[0]);
                             return a;
                           },void 0);
    }
    
    var data = [[9, 15], [64, 20], [26, 46], [30, 51], [74, 76], [53, 20], [66, 92], [90, 71], [31, 93], [75, 59], [24, 39], [99, 40], [13, 56], [95, 50], [3, 82], [43, 68], [2, 50]];
      result = boxBoxes(data);
    console.log(result.length,"boxes:",JSON.stringify(result));

    【讨论】:

      猜你喜欢
      • 2013-11-29
      • 2013-02-24
      • 2011-05-30
      • 1970-01-01
      • 2021-08-21
      • 2014-08-24
      • 2021-08-05
      • 1970-01-01
      相关资源
      最近更新 更多