高效地将盒子堆叠成最少数量的堆?

5
我在一个在线编码挑战中遇到了这个问题。
给定一个长宽为(l,h)的盒子列表,输出包含所有盒子所需的最小堆栈数,假设您可以将一个盒子叠放在另一个盒子上,如果其长度和宽度小于其他盒子。
我无法想出多项式时间解决方案。 我已经构建了一个蛮力解决方案,递归地创建所有可能的堆栈排列(从N个堆栈开始。然后对于每个堆栈,尝试将其放在其他堆栈的顶部。然后递归生成给定新堆栈排列的所有堆栈可能性),然后返回所需的最小堆栈数。
我通过一些优化加速了它:
- 如果您可以将盒子A叠放在盒子B和盒子C上,并且您可以将盒子B叠放在盒子C上,则不考虑在盒子C上叠放盒子A。 - 记忆化堆栈排列,仅考虑堆栈的底部和顶部
以下是此解决方案的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个盒子的问题。我相信这是指数时间复杂度。(因为堆栈排列的数量呈指数级增长)
我相信有一种多项式时间复杂度的解决方案,但我不知道是什么。其中一个问题是备忘录需要依赖于当前的堆栈排列,这意味着你需要进行更多的计算。

2
turn it into a graph problem. - Karoly Horvath
2
顺便提一下:如果您的代码按预期工作并希望使其更好,请尝试在此处发布:codereview.stackexchange.com - MooingRawr
1
允许旋转方块吗? - kraskevich
这看起来像是一个类似的问题:http://stackoverflow.com/questions/21712133 - Peter de Rivaz
3个回答

4
让我们建立一个图,每个盒子都有一个顶点,并且如果可以将盒子A叠放在盒子B上,则有从盒子A到盒子B的边缘。这个图是无环的(因为“可以叠放在顶部”是一种传递关系,而一个盒子不能叠放在自己的顶部)。
现在我们需要找到这个图的最小路径覆盖。这是一个标准问题,解决方法如下:
1.让我们建立一个二分图(原始图中的每个顶点在左侧和右侧部分各有两个“副本”)。对于原始图中的每个A->B边缘,在A的左侧副本和B的右侧副本之间有一条边缘。
2.答案是盒子数减去二分图中最大匹配的大小。
二分图有O(n)个顶点和O(n^2)个边缘。例如,如果我们使用Kuhn算法来查找匹配,总时间复杂度为O(n^3),其中n是盒子数。

@AbhishekBansal 一般情况下它是NP难的。然而,对于任何无环图(如我的答案中所述),它可以被简化为二分图问题,因此在这种特殊情况下它是多项式的。 - kraskevich
嗯... 在我看来没问题。+1 - Abhishek Bansal
@kraskevich 几个问题: - David Abrahams
糟糕,我不是想按回车键。1. 复制每个节点并使其成为二分图的优点是什么?2. 这个问题为什么不是 NP 难的? - David Abrahams
@DavidAbrahams 这个想法是答案总是原始图中顶点数减去最大匹配的大小。它显然可以在多项式时间内解决。为什么它是正确的?直观地说,如果一个盒子与某物匹配,它就会放在上面。可以证明每个匹配确实对应着一堆箱子(粗略地说,原始图是无环的,不会出现问题)。 - kraskevich

1
我最近也遇到了这个问题。我提出以下O(NlogN)的解决方案:
1)维护一个列表AllStkTops>,其中包含所有当前使用的盒子堆的顶部盒子。它将被初始化为空列表。
2)按盒子长度递减的顺序对盒子进行排序。(如果长度相等,则按面包宽度递增(是的,不是递减)的顺序排序)。

3)开始挑选箱子(从最长的开始),并将它们放入当前堆栈之一。对于第一个箱子,没有堆栈,因此它将成为第一个箱子堆栈的基础。第二个箱子要么放在第一个箱子的顶部,要么成为第二个堆栈的基础。随着我们继续进行这个过程,我们会意识到,对于手头上的任何箱子,其长度都将小于或等于所有堆栈的最顶端的箱子。因此,我们只需要担心它的宽度。从第一个堆栈开始,检查其宽度与所有当前堆栈的顶部匹配,并将其放置在满足以下两个条件的堆栈的顶部:i)长度和宽度都严格大于当前箱子,并且ii)宽度最小(为了最优化)。如果没有任何堆栈可以容纳此箱子,则创建一个以该箱子为基础的新堆栈。

请注意,由于我们只有在盒子的宽度大于此时所有堆栈顶部的宽度时才创建新的堆栈,因此所有堆栈顶部的宽度都会按升序排列。(对于长度相等的盒子,我们已经按宽度升序排序)。因此,我们可以使用二分搜索过程来找到一个最窄的堆栈顶部,其宽度足够大。但是,我们还需要确保其长度严格大于(而不仅仅是等于)给定盒子的长度。然而,我声称顶部的长度永远不会成为问题,因为:
i)盒子按长度递减顺序选择,因此顶部的长度肯定不会比给定盒子的长度小, ii)它也不能相等,因为我们已经按宽度递增顺序对长度相等的盒子进行了排序,因此得到前一个盒子的堆栈必须位于该堆栈顶部的左侧。
因此,我们可以使用二分搜索过程来找到当前盒子的最佳堆栈顶部。
我们可以重复上述过程,直到将所有盒子放入堆栈中。最后,计算AllStkTops中堆栈的数量。

这个算法的时间复杂度为O(NlogN),因为排序需要O(NlogN)时间,每个盒子进行二分查找需要O(logN)时间。

如果有人发现我的算法存在任何问题或缺陷,我很乐意回答任何问题。

干杯!


0

一开始看起来很简单,但考虑到可能性后,很快就变得棘手了。然而,我已经成功地在JS中实现了我的算法,我感到非常自信,而且JS有一些特殊之处,比如对象是引用类型,在这种特殊情况下对我帮助很大。

我从假设开始,认为我们可以将盒子旋转,使其长边在x轴上。然后,在Chrome中,给定的17个盒子在4~10毫秒之间完成,在FF中则需要15~25毫秒,结果最少需要5个盒子才能容纳所有17个盒子。

此外,我尝试了一个50个盒子的情况(每个盒子的尺寸在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));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接