嵌套框算法 - 基于嵌套玩偶但根本不同?

时间:2016-11-25 22:40:16

标签: java algorithm dynamic-programming graph-theory theory

我试图解决与SPOJ嵌套玩偶问题相关的问题,其中使用具有二维底部的框而不是在单个尺度参数中不同的玩偶。我有一个算法,但我对这个问题背后的实际理论以及是否存在更好的方法非常浑浊。任何人都可以帮助我更好地理解问题,也许找到更好的算法吗?

作为复习,嵌套玩偶问题如下:

鉴于各种尺寸的 N Matryoshka娃娃,找到最小数量的嵌套娃娃,这些娃娃在将娃娃最佳地嵌套在彼此之后仍然存在。对于每个嵌套的玩偶,如果最外面的玩偶大小 S 那么它不包含玩偶,或者它包含一个嵌套的玩偶,其大小严格小于 S

我不知道这个问题的全部细节,但是通过阅读它我相信嵌套玩偶问题可以通过增加大小来分类玩偶并重复从中提取最长增加子序列(LIS)来解决。大小序列,通过选择利用最大娃娃的子序列来打破关系。嵌套玩偶的数量将是提取的子序列的数量。我认为这种贪婪的算法有效,因为:

a)减少其中一个子序列的长度会引入新玩偶,这些玩偶无法减少未来步骤中发现的嵌套玩偶的数量("越少越好")

b)在子序列中更换一个玩偶必须用一个更大的玩偶替换剩下的玩偶中的一个小玩偶,这不能减少在未来步骤中找到的嵌套玩偶的数量("越小越好" )

这意味着可以使用良好的LIS算法在O(N log N)中解决问题。

但盒子问题不同: 给定N个具有不同底部尺寸的打开的盒子,找到最佳数量的盒子堆叠,这些盒子堆叠最佳地将盒子彼此嵌套在一起。对于每个盒子堆叠,如果最外面的盒子具有 W x H 的尺寸,那么它不包含任何盒子,或者它包含一个盒子堆栈,其宽度和高度严格更小 W H

这意味着盒子没有总排序 - 如果盒子A不适合盒子B,它并不意味着盒子B的尺寸与A相同或者它适合盒子A,不像Matryoshka娃娃。

我不知道我是否正确,但我认为通过反复提取LIS(或者更确切地说,彼此适合的最长序列的盒子)可以找到最佳解决方案已经不再适用了。 ),主要是因为没有好办法打破关系。如果我们在1x17盒子和5x4盒子之间进行比较,那么具有更大面积的盒子对于将来的步骤仍然会更有用。尝试所有绑定的LIS听起来像指数运行时。我是对的,还是真的有贪婪的方法来做到这一点?

我只发现了另外一篇关于此事的帖子(Stacking boxes into fewest number of stacks efficiently?),建议使用图论方法来解决问题。我对图论的经验很少,所以我不知道这种方法是如何工作的。我基本上盲目地用他们的话来制作一个盒子的二分图,断言盒子的数量=(盒子的数量 - 最大匹配的大小)。然后,我基于伪代码在Java中实现了Fork Fulkerson算法,但没有完全理解它是如何实际解决问题的。我已经尽力用我的思维过程来注释代码,但它让我觉得这种方法与嵌套玩偶解决方案如此不同,当我在1小时内遇到这种情况时需要150多行。难道没有更容易解决问题的方法吗?

代码:

import java.util.*;

public class NestedBoxes {
    private static final int SOURCE_INDEX = -1;
    private static final int SINK_INDEX = -2;

    private NestedBoxes() {
        // Unused
    }

    public static void main(String args[] ) throws Exception {
        // Get box dimensions from user input
        Scanner sc = new Scanner(System.in);
        int numBoxes = sc.nextInt();
        List<Rectangle> boxes = new ArrayList<>();
        for (int i = 0; i < numBoxes; i++) {
            Rectangle box = new Rectangle(sc.nextInt(), sc.nextInt());
            boxes.add(box);
        }

        // Sort boxes by bottom area as a useful heuristic
        Collections.sort(boxes, (b1, b2) -> Integer.compare(b1.width * b1.height, b2.width * b2.height));

        // Make a bipartite graph based on which boxes fit into each other, and
        //  add a source linking to all boxes and a sink linked by all boxes.
        // Forward edges go from the left (lower index) nodes to the right (higher index) nodes.
        // Each forward edge has a corresponding backward edge in the bipartite section.
        // Only one of the two edges are active at any point in time.
        Map<Integer, Map<Integer, BooleanVal>> graphEdges = new HashMap<>();
        Map<Integer, BooleanVal> sourceMap = new HashMap<>();
        graphEdges.put(SOURCE_INDEX, sourceMap);
        graphEdges.put(SINK_INDEX, new HashMap<>()); // Empty adjacency list for the sink
        for (int i = 0; i < numBoxes; i++) {
            // TreeMaps make the later DFS step prefer reaching the sink over other nodes, and prefer
            //  putting boxes into the smallest fitting box first, speeding up the search a bit since
            //  log(N) is not that bad compared to a large constant factor.
            graphEdges.put(i, new TreeMap<>());
            // Each node representing a box is duplicated in a bipartite graph, where node[i]
            //  matches with node[numBoxes + i] and represent the same box
            graphEdges.put(numBoxes + i, new TreeMap<>());
        }
        for (int i = 0; i < boxes.size(); i++) {
            // Boolean pointers are used so that backward edges ("flow") and
            //  forward edges ("capacity") are updated in tandem, maintaining that
            //  only one is active at any time.
            sourceMap.put(i, new BooleanPtr(true)); // Source -> Node
            graphEdges.get(numBoxes + i).put(SINK_INDEX, new BooleanPtr(true)); // Node -> Sink
            for (int j = i + 1; j < boxes.size(); j++) {
                if (fitsIn(boxes.get(i), boxes.get(j))) {
                    BooleanVal opening = new BooleanPtr(true);
                    graphEdges.get(i).put(numBoxes + j, opening); // Small box -> Big box
                    graphEdges.get(numBoxes + j).put(i, new Negation(opening)); // Small box <- Big box
                }
            }
        }
        Deque<Integer> path; // Paths are represented as stacks where the top is the first node in the path
        Set<Integer> visited = new HashSet<>(); // Giving the GC a break
        // Each DFS pass takes out the capacity of one edge from the source
        //  and adds a single edge to the bipartite matching generated.
        // The algorithm automatically backtracks if a suboptimal maximal matching is found because
        //  the path would take away edges and add new ones in if necessary.
        // This happens when the path zigzags using N backward edges and (N + 1) forward edges -
        //  removing a backward edge corresponds to removing a connection from the matching, and using extra
        //  forward edges will add new connections to the matching.
        // So if no more DFS passes are possible, then no amount of readjustment will increase the size
        //  of the matching, so the number of passes equals the size of the maximum matching of the bipartite graph.
        int numPasses = 0;
        while ((path = depthFirstSearch(graphEdges, SOURCE_INDEX, SINK_INDEX, visited)) != null) {
            visited.clear();
            Integer current = SOURCE_INDEX;
            path.pop();
            for (Integer node : path) {
                // Take out the edges visited.
                //  Taking away any backward edges automatically adds back the corresponding forward edge,
                //  and similarly removing a forward edge adds back the backward edge.
                graphEdges.get(current).get(node).setBoolValue(false);
                current = node;
            }
            numPasses++;
        }

        // Print out the stacks made from the boxes. Here, deleted forward edges / available backward edges
        //  represent opportunities to nest boxes that have actually been used in the solution.
        System.out.println("Box stacks:");
        visited.clear();
        for (int i = 0; i < numBoxes; i++) {
            Integer current = i;
            if (visited.contains(current)) {
                continue;
            }
            visited.add(current);
            boolean halt = false;
            while (!halt) {
                halt = true;
                System.out.print(boxes.get(current));
                for (Map.Entry<Integer, BooleanVal> entry : graphEdges.get(current).entrySet()) {
                    int neighbor = entry.getKey() - numBoxes;
                    if (!visited.contains(neighbor) && !entry.getValue().getBoolValue()) {
                        System.out.print("->");
                        visited.add(neighbor);
                        current = neighbor;
                        halt = false;
                        break;
                    }
                }
            }
            System.out.println();
        }
        System.out.println();

        // Let a box-stack be a set of any positive number boxes nested into one another, including 1.
        // Beginning with each box-stack being a single box, we can nest them to reduce the box-stack count.
        // Each DFS pass, or edge in the maximal matching, represents a single nesting opportunity that has
        //  been used. Each used opportunity removes one from the number of box-stacks. so the total number
        //  of box-stacks will be the number of boxes minus the number of passes.
        System.out.println("Number of box-stacks: " + (numBoxes - numPasses));
    }

    private static Deque<Integer> depthFirstSearch(Map<Integer, Map<Integer, BooleanVal>> graphEdges,
                                           int source, int sink, Set<Integer> visited) {
        if (source == sink) {
            // Base case where the path visits only one node
            Deque<Integer> result = new ArrayDeque<>();
            result.push(sink);
            return result;
        }

        // Get all the neighbors of the source node
        Map<Integer, BooleanVal> neighbors = graphEdges.get(source);
        for (Map.Entry<Integer, BooleanVal> entry : neighbors.entrySet()) {
            Integer neighbor = entry.getKey();
            if (!visited.contains(neighbor) && entry.getValue().getBoolValue()) {
                // The neighbor hasn't been visited before, and the edge is active so the
                //  DFS attempts to include this edge into the path.
                visited.add(neighbor);
                // Trying to find a path from the neighbor to the sink
                Deque<Integer> path = depthFirstSearch(graphEdges, neighbor, sink, visited);
                if (path != null) {
                    // Adds the source onto the path found
                    path.push(source);
                    return path;
                } else {
                    // Pretend we never visited the neighbor and move on
                    visited.remove(neighbor);
                }
            }
        }
        // No paths were found
        return null;
    }

    // Interface for a mutable boolean value
    private interface BooleanVal {
        boolean getBoolValue();
        void setBoolValue(boolean val);
    }

    // A boolean pointer
    private static class BooleanPtr implements BooleanVal {
        private boolean value;

        public BooleanPtr(boolean value) {
            this.value = value;
        }

        @Override
        public boolean getBoolValue() {
            return value;
        }

        @Override
        public void setBoolValue(boolean value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "" + value;
        }
    }

    // The negation of a boolean value
    private static class Negation implements BooleanVal {
        private BooleanVal ptr;

        public Negation(BooleanVal ptr) {
            this.ptr = ptr;
        }

        @Override
        public boolean getBoolValue() {
            return !ptr.getBoolValue();
        }

        @Override
        public void setBoolValue(boolean val) {
            ptr.setBoolValue(!val);
        }

        @Override
        public String toString() {
            return "" + getBoolValue();
        }
    }

    // Method to find if a rectangle strictly fits inside another
    private static boolean fitsIn(Rectangle rec1, Rectangle rec2) {
        return rec1.height < rec2.height && rec1.width < rec2.width;
    }

    // A helper class representing a rectangle, or the bottom of a box
    private static class Rectangle {
        public int width, height;

        public Rectangle(int width, int height) {
            this.width = width;
            this.height = height;
        }

        @Override
        public String toString() {
            return String.format("(%d, %d)", width, height);
        }
    }
}

1 个答案:

答案 0 :(得分:1)

是的,有一个更简单(也更有效)的解决方案。

让我们按照它们的宽度对盒子进行排序(如果两个盒子的宽度相同,则按其高度的相反顺序排序)。很明显,我们只能将一个盒子嵌入到它后面的盒子中。因此,我们希望将其分成多个增加的子序列(现在仅考虑高度)。有一个定理说,序列可以分成的最小增加子序列的数量等于最长非增长的长度(即,不严格地减少子序列)。

总而言之,解决方案是这样的:

  1. 按宽度对框进行排序。如果宽度相同,则按相反的高度比较它们。

  2. 丢掉宽度,然后计算高度最长的非增加子序列的长度(按照排序后的顺序)。这是问题的答案。就是这样。

  3. 很明显,如果正确实施,此解决方案可以在O(N log N)时间内运行。