Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Minimum rectangles required to cover a given rectangular area

I have a rectangular area of dimension: n*m. I also have a smaller rectangle of dimension: x*y. What will be the minimum number of smaller rectangles required to cover all the area of the bigger rectangle?

It's not necessary to pack the smaller rectangles. They are allowed to overlap with each other, cross the borders of the bigger rectangle if required. The only requirement is that we have to use the fewest number of x*y rectangles.

Another thing is that we can rotate the smaller rectangles if required (90 degrees rotation I mean), to minimise the number.

n,m,x and y: all are natural numbers. x, y need not be factors of n,m.

I couldn't solve it in the time given, neither could I figure out an approach. I initiated by taking up different cases of n,m being divisible by x,y or not.

update

sample test cases:

  • n*m = 3*3, x*y = 2*2. Result should be 4
  • n*m = 5*6, x*y = 3*2. Result should be 5
  • n*m = 68*68, x*y = 9*8. Result should be 65
like image 913
Akeshwar Jha Avatar asked Sep 03 '16 11:09

Akeshwar Jha


1 Answers

(UPDATE: See newer version below.)

I think (but I have no proof at this time) that irregular tilings can be discarded, and finding the optimal solution means finding the point at which to switch the direction of the tiles.

You start off with a basic grid like this:

tiling - basic grid

and the optimal solution will take one of these two forms:

solution type 1 solution type 2

So for each of these points, you calculate the number of required tiles for both options:

all grid points


This is a very basic implementation. The "horizontal" and "vertical" values in the results are the number of tiles in the non-rotated zone (indicated in pink in the images).

The algorithm probably checks some things twice, and could use some memoization to speed it up.

(Testing has shown that you need to run the algorithm a second time with the x and y parameter switched, and that checking both types of solution is indeed necessary.)

function rectangleCover(n, m, x, y, rotated) {
    var width = Math.ceil(n / x), height = Math.ceil(m / y);
    var cover = {num: width * height, rot: !!rotated, h: width, v: height, type: 1};
    for (var i = 0; i <= width; i++) {
        for (var j = 0; j <= height; j++) {
            var rect = i * j;

            var top = simpleCover(n, m - y * j, y, x);
            var side = simpleCover(n - x * i, y * j, y, x);
            var total = rect + side + top;
            if (total < cover.num) {
                cover = {num: total, rot: !!rotated, h: i, v: j, type: 1};
            }
            var top = simpleCover(x * i, m - y * j, y, x);
            var side = simpleCover(n - x * i, m, y, x);
            var total = rect + side + top;
            if (total < cover.num) {
                cover = {num: total, rot: !!rotated, h: i, v: j, type: 2};
            }
        }
    }
    if (!rotated && n != m &&  x != y) {
        var c = rectangleCover(n, m, y, x, true);
        if (c.num < cover.num) cover = c;
    }
    return cover;

    function simpleCover(n, m, x, y) {
        return (n > 0 && m > 0) ? Math.ceil(n / x) * Math.ceil(m / y) : 0;
    }
}
document.write(JSON.stringify(rectangleCover(3, 3, 2, 2)) + "<br>");
document.write(JSON.stringify(rectangleCover(5, 6, 3, 2)) + "<br>");
document.write(JSON.stringify(rectangleCover(22, 18, 5, 3)) + "<br>");
document.write(JSON.stringify(rectangleCover(1000, 1000, 11, 17)));

This is the counter-example Evgeny Kluev provided: (68, 68, 9, 8) which returns 68 while there is a solution using just 65 rectangles, as demonstrated in this image:

counter-example (68,68,9,8)


Update: improved algorithm

The counter-example shows the way for a generalisation of the algorithm: work from the 4 corners, try all unique combinations of orientations, and every position of the borders a, b, c and d between the regions; if a rectangle is left uncovered in the middle, try both orientations to cover it:

rectangle covering from the corners

Below is a simple, unoptimised implementation of this idea; it probably checks some configurations multiple times, and it takes 6.5 seconds for the 11×17/1000×1000 test, but it finds the correct solution for the counter-example and the other tests from the previous version, so the logic seems sound.

rotations

These are the five rotations and the numbering of the regions used in the code. If the large rectangle is a square, only the first 3 rotations are checked; if the small rectangles are squares, only the first rotation is checked. X[i] and Y[i] are the size of the rectangles in region i, and w[i] and h[i] are the width and height of region i expressed in number of rectangles.

function rectangleCover(n, m, x, y) {
    var X = [[x,x,x,y],[x,x,y,y],[x,y,x,y],[x,y,y,x],[x,y,y,y]];
    var Y = [[y,y,y,x],[y,y,x,x],[y,x,y,x],[y,x,x,y],[y,x,x,x]];
    var rotations = x == y ? 1 : n == m ? 3 : 5;
    var minimum = Math.ceil((n * m) / (x * y));
    var cover = simpleCover(n, m, x, y);

    for (var r = 0; r < rotations; r++) {
        for (var w0 = 0; w0 <= Math.ceil(n / X[r][0]); w0++) {
            var w1 = Math.ceil((n - w0 * X[r][0]) / X[r][1]);
            if (w1 < 0) w1 = 0;
            for (var h0 = 0; h0 <= Math.ceil(m / Y[r][0]); h0++) {
                var h3 = Math.ceil((m - h0 * Y[r][0]) / Y[r][3]);
                if (h3 < 0) h3 = 0;
                for (var w2 = 0; w2 <= Math.ceil(n / X[r][2]); w2++) {
                    var w3 = Math.ceil((n - w2 * X[r][2]) / X[r][3]);
                    if (w3 < 0) w3 = 0;
                    for (var h2 = 0; h2 <= Math.ceil(m / Y[r][2]); h2++) {
                        var h1 = Math.ceil((m - h2 * Y[r][2]) / Y[r][1]);
                        if (h1 < 0) h1 = 0;
                        var total = w0 * h0 + w1 * h1 + w2 * h2 + w3 * h3;
                        var X4 = w3 * X[r][3] - w0 * X[r][0];
                        var Y4 = h0 * Y[r][0] - h1 * Y[r][1];
                        if (X4 * Y4 > 0) {
                            total += simpleCover(Math.abs(X4), Math.abs(Y4), x, y);
                        }
                        if (total == minimum) return minimum;
                        if (total < cover) cover = total;
                    }
                }
            }
        }
    }
    return cover;

    function simpleCover(n, m, x, y) {
        return Math.min(Math.ceil(n / x) * Math.ceil(m / y),
                        Math.ceil(n / y) * Math.ceil(m / x));
    }
}

document.write("(3, 3, 2, 2) &rarr; " + rectangleCover(3, 3, 2, 2) + "<br>");
document.write("(5, 6, 3, 2) &rarr; " + rectangleCover(5, 6, 3, 2) + "<br>");
document.write("(22, 18, 5, 3) &rarr; " + rectangleCover(22, 18, 5, 3) + "<br>");
document.write("(68, 68, 8, 9) &rarr; " + rectangleCover(68, 68, 8, 9) + "<br>");

Update: fixed calculation of center region

As @josch pointed out in the comments, the calculation of the width and height of the center region 4 is not done correctly in the above code; Sometimes its size is overestimated, which results in the total number of rectangles being overestimated. An example where this happens is (1109, 783, 170, 257) which returns 23 while there exists a solution of 22. Below is a new code version with the correct calculation of the size of region 4.

function rectangleCover(n, m, x, y) {
    var X = [[x,x,x,y],[x,x,y,y],[x,y,x,y],[x,y,y,x],[x,y,y,y]];
    var Y = [[y,y,y,x],[y,y,x,x],[y,x,y,x],[y,x,x,y],[y,x,x,x]];
    var rotations = x == y ? 1 : n == m ? 3 : 5;
    var minimum = Math.ceil((n * m) / (x * y));
    var cover = simpleCover(n, m, x, y);

    for (var r = 0; r < rotations; r++) {
        for (var w0 = 0; w0 <= Math.ceil(n / X[r][0]); w0++) {
            var w1 = Math.ceil((n - w0 * X[r][0]) / X[r][1]);
            if (w1 < 0) w1 = 0;
            for (var h0 = 0; h0 <= Math.ceil(m / Y[r][0]); h0++) {
                var h3 = Math.ceil((m - h0 * Y[r][0]) / Y[r][3]);
                if (h3 < 0) h3 = 0;
                for (var w2 = 0; w2 <= Math.ceil(n / X[r][2]); w2++) {
                    var w3 = Math.ceil((n - w2 * X[r][2]) / X[r][3]);
                    if (w3 < 0) w3 = 0;
                    for (var h2 = 0; h2 <= Math.ceil(m / Y[r][2]); h2++) {
                        var h1 = Math.ceil((m - h2 * Y[r][2]) / Y[r][1]);
                        if (h1 < 0) h1 = 0;
                        var total = w0 * h0 + w1 * h1 + w2 * h2 + w3 * h3;
                        var X4 = n - w0 * X[r][0] - w2 * X[r][2];
                        var Y4 = m - h1 * Y[r][1] - h3 * Y[r][3];
                        if (X4 > 0 && Y4 > 0) {
                            total += simpleCover(X4, Y4, x, y);
                        } else {
                            X4 = n - w1 * X[r][1] - w3 * X[r][3];
                            Y4 = m - h0 * Y[r][0] - h2 * Y[r][2];
                            if (X4 > 0 && Y4 > 0) {
                                total += simpleCover(X4, Y4, x, y);
                            }
                        }
                        if (total == minimum) return minimum;
                        if (total < cover) cover = total;
                    }
                }
            }
        }
    }
    return cover;

    function simpleCover(n, m, x, y) {
        return Math.min(Math.ceil(n / x) * Math.ceil(m / y),
                        Math.ceil(n / y) * Math.ceil(m / x));
    }
}

document.write("(3, 3, 2, 2) &rarr; " + rectangleCover(3, 3, 2, 2) + "<br>");
document.write("(5, 6, 3, 2) &rarr; " + rectangleCover(5, 6, 3, 2) + "<br>");
document.write("(22, 18, 5, 3) &rarr; " + rectangleCover(22, 18, 5, 3) + "<br>");
document.write("(68, 68, 9, 8) &rarr; " + rectangleCover(68, 68, 9, 8) + "<br>");
document.write("(1109, 783, 170, 257) &rarr; " + rectangleCover(1109, 783, 170, 257) + "<br>");

Update: non-optimality and recursion

It is indeed possible to create input for which the algorithm does not find the optimal solution. For the example (218, 196, 7, 15) it returns 408, but there is a solution with 407 rectangles. This solution has a center region sized 22×14, which can be covered by three 7×15 rectangles; however, the simpleCover function only checks options where all rectangles have the same orientation, so it only finds a solution with 4 rectangles for the center region.

counter-example (218,196,15,7)

This can of course be countered by using the algorithm recursively, and calling rectangleCover again for the center region. To avoid endless recursion, you should limit the recursions depth, and use simpleCover once you've reached a certain recursion level. To avoid the code becoming unusably slow, add memoization of intermediate results (but don't use results that were calculated in a deeper recursion level for a higher recursion level).

When adding one level of recursion and memoization of intermediate results, the algorithm finds the optimal solution of 407 for the example mentioned above, but of course takes a lot more time. Again, I have no proof that adding a certain recursion depth (or even unlimited recursion) will result in the algorithm being optimal.

like image 91