园区规划中的最少正方形划分问题 | 豆包MarsCode AI刷题

122 阅读3分钟

题目描述

原题链接:园区规划中的最少正方形划分问题

简单来讲,就是给定一个 m × n 的矩形,求至少需要多少个正方形,才能恰好拼成这个 m × n 的矩形

从直觉的角度

对于一个 3 × 2 的矩形,直观上我们会选择短边,先放置一个 2 × 2 的正方形,然后剩下一个 1 × 2 的区域。继续选择短边,我们需要一个 1 × 1 的正方形,最终只剩下一个 1 × 1 的正方形,总共需要 3 个正方形,这与样例相符。

这种直觉上的策略,即每一轮都选择短边来形成一个最大的正方形,似乎是可行的。但是,从 13 × 11 的样例中,我们可以看到这种方法的局限性。

image.png

直觉上的贪心策略得到的结果是需要 8 个正方形,但最优解是 6 个正方形,具体的划分方案如下:

image.png

我们可以看到,最优方案中有一个较小的正方形,由于这种特殊结构,最优解很难通过贪心或动态规划得到。在没有更好的解决方案,且数据量不大(m 和 n 的值小于 20)的情况下,暴力搜索可能是最佳选择。

暴搜方式

如果选择暴力搜索,我们需要考虑如何搜索以覆盖所有可能的方案。

一种方法是固定左上角的点 (i, j),从大到小遍历可能的正方形边长,直到边长为 1,本轮遍历结束:

image.png

如果 j > n,说明第 i 行的小正方形已经填满,需要进行 i++,并且 j 重置为 1,从下一行开始遍历未填充的小正方形。通过这种方式,在遍历过程中,我们可能会得到如下方案,其中红色表示当前固定的左上角节点,灰色部分表示已经填充的矩形,蓝色代表还未填充的矩形:

image.png

记录节点 (i, j) 是否填充过,只需要一个布尔数组即可。

在填充过程中,只需检查对应节点是否已被填充,如果已经填充过,再次填充会导致矩形重叠,说明当前方案不可行,应立即退出当前轮次的搜索。

状态复原小 tips

由于使用布尔数组存储节点 (i, j) 是否填充过,可以利用位运算,只用一个函数就实现填充或取消填充。

如果 (i, j)false,与 true 进行一次异或操作,可以变为 true,表示进行一次填充操作。若再与 true 进行一次异或操作,就可以复原成 false,表示取消填充。

完整 Java 代码

public class Main {

    private static boolean[][] matrix;
    private static int x, y;
    private static int ans;

    private static boolean check(int m, int n, int len) {
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                if ( matrix[m + i][n + j] ) {
                    return false;
                }
            }
        }
        return true;
    }

    private static void fill(int m, int n, int len) {
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                matrix[m + i][n + j] ^= true;
            }
        }
    }

    private static void dfs(int m, int n, int cnt) {
        if(cnt >= ans) {
            return;
        }
        if(m == x + 1) {
            ans = cnt;
            return;
        }
        if(n > y) {
            dfs(m + 1, 1, cnt);
        }
        boolean flag = true;
        for(int j = n; j <= y; j++) {
            // 尝试填充该格子
            if( !matrix[m][j] ) {
                flag = false;
                for(int i = Math.min(y - j + 1, x - m + 1); i >= 1; i--) {
                    if( check(m, j, i) ) {
                        // 填充
                        fill(m, j, i);
                        dfs(m, n + i, cnt + 1);
                        // 复原
                        fill(m, j, i);
                    }
                }
                // 当前格子的所有填充情况已考虑
                break;
            }

        }
        // 第 m 行的格子都已填充完
        if( flag ) {
            dfs(m + 1, 1, cnt);
        }
    }

    public static int solution(int m, int n) {
        // write code here
        x = m;
        y = n;
        matrix = new boolean[x + 1][y + 1];
        ans = Integer.MAX_VALUE;
        dfs(1, 1, 0);
        return ans;
    }

    public static void main(String[] args) {
        System.out.println(solution(3, 2) == 3);
        System.out.println(solution(13, 11) == 6);
        System.out.println(solution(4, 4) == 1);
    }
}