Fenrier Lab

算法题:矩形分割

问题 给定一个矩形框作为背景,在其中放置一个方块,可以将大矩形分割成若干个大小不等的矩形区域,放置的方块越多,分割方案也越多,如下图所示。

现在给出大矩形的坐标,以及若干方块的坐标(方块完全位于大矩形内部,且它们之间不相交,并且放置顺序固定),要求编程输出分割后最大的矩形区域坐标。

思路

首先分析放置一个矩形的分割情况,一个方块可以把背景分割成 4 个矩形区域,其中每个点可以横向或者纵向绘制线段,因此总共有 (2^4 = 16) 种分割方案。

在放置第二个矩形物体的时候,会出现两种情况,一种情况是落入前一步的小矩形内(下图左边),第二情况是破坏原来的分割方案(下图右边)。

对于第一种情况,置入的物体可以继续分割其所在的矩形,而第二种情况则直接淘汰掉,表现在树结构上可以看作是剪枝

加入更多的物体其实就是不断的对树进行扩展,并在这一过程中剪枝。最终可以得到多条可行的路径,每条路径都是一个可行的分割方案,我们只需追溯这些路径就可以看到方块对背景的分割情况,选择一个拥有最大矩形子区域的方案即可。

下面我们采用动态规划的方法来描述解决方案: 原问题:给定一个矩形,以及 n 个矩形方块,求解方块对矩形的所有分割方案; 终止解:给定一个矩形,以及一个方块,可以把矩形分割成4个矩形区域,且共有16种方案; 递归问题:已知一个矩形,以及 n - 1 个方块对其进行分割的所有方案,求解再添加一个方块得到的所有切割方案。

实现

首先,我们实现一个函数,其目的是计算一个方块对矩形的一种分割方案

/**
     * 矩形被方块分割成 4 个矩形
     * @param rect 背景矩形坐标
     * @param obj 方块坐标
     * @param axisDirection 方块四个点的分割方向,0代表横向,1代表纵向
     * @return 四个子区域的坐标
     * */
    public static List<int[]> splitRect(int[] rect, int[] obj, int[] axisDirection) {

        int left = 0;
        int right = 1;
        int top = 2;
        int bottom = 3;

        int l = rect[left];
        int r = rect[right];
        int t = rect[top];
        int b = rect[bottom];
        int ol = obj[left];
        int or = obj[right];
        int ot = obj[top];
        int ob = obj[bottom];
        int[][] rects = new int[4][4];
        rects[0] = new int[]{l, ol, t, b};
        rects[1] = new int[]{l, r, t, ot};
        rects[2] = new int[]{or, r, t, b};
        rects[3] = new int[]{l, r, ob, b};
        if(axisDirection[0] == 0) {
            rects[0][top] = obj[top];
            rects[1][bottom] = obj[top];
        }else {
            rects[0][right] = obj[left];
            rects[1][left] = obj[left];
        }

        if(axisDirection[1] == 0) {
            rects[1][bottom] = obj[top];
            rects[2][top] = obj[top];
        }else {
            rects[1][right] = obj[right];
            rects[2][left] = obj[right];
        }

        if(axisDirection[2] == 0) {
            rects[2][bottom] = obj[bottom];
            rects[3][top] = obj[bottom];
        }else{
            rects[2][left] = obj[right];
            rects[3][right] = obj[right];
        }

        if(axisDirection[3] == 0) {
            rects[0][bottom] = obj[bottom];
            rects[3][top] = obj[bottom];
        } else {
            rects[0][right] = obj[left];
            rects[3][left] = obj[left];
        }

        return Arrays.asList(rects);
    }

这里的 axisDirection 参数规定了方块四个角点的分割线方向,当其都为 0 的时,即都横向分割,那么获得的分割矩形形如下图

前面我们提到,一个方块可以有 16 种分割方案,其中每种方案就是由这里的 axisDirection 完全确定的,为了呈现所有分割方案,我们需要列出所有可能的 axisDirection 值,即 0 和 1 构成的四元素数组的全排列

    /**
     * 给定数字 0...n-1,求由这些数字组成长度为 len 的数组的全排列
     * 终止解:给定数字 0...n-1,由这些数字组成长度为 1 的数组的全部情况为 [0], [1], ... [n-1]
     * 递归问题:给定数字 0...n-1,以及由这些数字组成长度为 len - 1 的数组的全排列,求由这些数字组成长度为 len 的数组的全排列
     * */
public static List<List<Integer>> permutation(int len, int n) {

        // 终止条件
        if(len == 1) { // 构造列表 [[0], [1], ... [n-1]]
            ArrayList<List<Integer>> list = new ArrayList<>();
            for (int i = 0; i < n; i++) {
                ArrayList<Integer> ls = new ArrayList<>();
                ls.add(i);
                list.add(ls);
            }
            return list;
        }

        // 递归求解子问题
        List<List<Integer>> list = permutation(len - 1, n);

        return list.stream()
                .flatMap(ls -> {
                    ArrayList<List<Integer>> lists = new ArrayList<>();
                    for (int i = 0; i < n; i++) {
                        ArrayList<Integer> ls2 = new ArrayList<>(ls);
                        ls2.add(i);
                        lists.add(ls2);
                    }
                    return lists.stream();
                })
                .collect(Collectors.toList());

    }

下面我们在实现两个工具函数,用于判断矩形之间的关系

  /**
    * 判断两个矩形是否有交集
    * 如果某一个矩形的左边大于另一个矩形的右边,或某矩形的上边小于另一矩形的下边,则两者不相交,否则相交
    * */
  def isRectOverlap(rect1: Array[Int], rect2: Array[Int]): Boolean = {

    !(rect1(0) > rect2(1) || rect1(1) < rect2(0) || rect1(2) > rect2(3) || rect1(3) < rect2(2))

  }

  /**
    * 判断两个矩形是否是包含关系
    *
    * @param rect1 外部矩形
    * @param rect2 内部矩形
    * */
  def isRectContains(rect1: Array[Int], rect2: Array[Int]): Boolean = {

    rect1(0) < rect2(0) && rect1(1) > rect2(1) && rect1(2) < rect2(2) && rect1(3) > rect2(3)

  }

最后,我们实现多个方块分割方案函数

/**
     * 给定一个矩形,以及 n 个方块,求解对矩形的所有分割方案
     * 终止解:给定一个矩形,以及一个方块,可以把矩形分割成4个小矩形区域,且共有16种方案
     * 递归问题:已知一个矩形,以及 n - 1 个方块对其进行分割的所有方案,求解再添加一个方块得到的所有切割方案
     * @param rect 被分割矩形
     * @param objs 方块列表
     * */
    public static List<List<int[]>> splitRect(int[] rect, List<int[]> objs) {

        if(objs.isEmpty()) {
            ArrayList<List<int[]>> lists = new ArrayList<>();
            ArrayList<int[]> list = new ArrayList<>();
            list.add(rect);
            lists.add(list);
            return lists;
        }

        if(objs.size() == 1) {
            return splitRect(rect, objs.get(0));
        }

        int[] obj = objs.remove(0);

        // 求解子问题
        List<List<int[]>> plans = splitRect(rect, objs);

        // 向目前的分割方案中添加一个方块
        return plans.stream()
                .flatMap(plan -> {

                    // 判断当前方块是否与现有区域相交
                    boolean intersect = plan.stream()
                            .anyMatch(rt -> isRectOverlap(rt, obj) && !isRectContains(rt, obj));

                    if(intersect) {
                        return Stream.empty();
                    }

                    // 找到包含当前方块的区域,用于后续分割
                    Optional<int[]> contains = plan.stream()
                            .filter(rt -> isRectContains(rt, obj))
                            .findFirst();
                    if(!contains.isPresent()) {
                        return Stream.empty();
                    }

                    // 不包含当前方块的区域列表
                    List<int[]> plan2 = plan.stream()
                            .filter(rt -> !isRectContains(rt, obj))
                            .collect(Collectors.toList());
                    int[] rt = contains.get();

                    return splitRect(rt, obj)
                            .stream()
                            .map(list -> {
                                List<int[]> newPlan = new ArrayList<>(plan2);
                                newPlan.addAll(list);
                                return newPlan;
                            });
                })
                .filter(list -> !list.isEmpty())
                .collect(Collectors.toList());

    }

至此,我们实现了列举多个方块分割背景矩形的所有方案的算法。测试一下

ArrayList<int[]> objs = new ArrayList<>();
int[] r1 = {150, 250, 150, 250};
objs.add(r1);
int[] r2 = {300, 350, 180, 300};
objs.add(r2);
List<List<int[]>> lists = splitRect(new int[]{100, 800, 100, 600}, objs);

要解决最初的问题,只需要找到这些方案中面积最大的子区域即可。

本文遵守 CC-BY-NC-4.0 许可协议。

Creative Commons License

欢迎转载,转载需注明出处,且禁止用于商业目的。