16

Background

I have a rectangular area divided up into squares (this is for a game I'm making). I'm representing this in my code as a simple two-dimensional boolean array:

  ┌──┬──┬──┬──┬──┐
  │  │  │  │  │  │ X = X-value, also increasing width
  ├──┼──┼──┼──┼──┤ Y = Y-value, also increasing length
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
  │  │  │  │  │  │
  ├──┼──┼──┼──┼──┤
^ │  │  │  │  │  │
Y └──┴──┴──┴──┴──┘
   X >

Some of the squares can be taken up in rectangles by buildings, etc. like this input (** = taken):

┌──┬──┬──┬──┬──┐
│**│**│  │  │  │ 3 taken rectangles
├──┼──┼──┼──┼──┤
│**│**│**│**│  │
├──┼──┼──┼──┼──┤
│  │  │**│**│  │
├──┼──┼──┼──┼──┤
│  │  │**│**│  │
├──┼──┼──┼──┼──┤
│**│  │**│**│  │
└──┴──┴──┴──┴──┘

In the 2D boolean array, "taken" squares are set to true and "open, non-taken" squares are set to false.

My problem

I need to find all "open" rectangles (non-taken) that are of a specific size. This is because I need to find all the possible spaces to put the next building. For example, from the previous figure, if I wanted to get all "open" 1x2 rectangles I should get these outputs:

┌──┬──┬──┬──┬──┐
│**│**│1 │12│2 │ 3 taken rectangles (input)
├──┼──┼──┼──┼──┤ 4 open 1x2 rectangles (output)
│**│**│**│**│  │ Open rectangles are numbered
├──┼──┼──┼──┼──┤ 
│3 │3 │**│**│  │ Rectangles can overlap.
├──┼──┼──┼──┼──┤ The '12' square represents the overlapping
│4 │4 │**│**│  │ of rectangle 1 and 2.
├──┼──┼──┼──┼──┤ 
│**│  │**│**│  │ (Haha, my diagram kind of looks like old Minesweeper)
└──┴──┴──┴──┴──┘

What I've done

Here's I've tested (brute-force search, code is C# and a bit of pseudocode ):

List<Rectangle> findOpenSpaces(boolean[][] area, int width, int length) {
    List<Rectangle> openSpaces = new List<Rectangle>();
    boolean open = true;
    // Loop through all rectangles with size "width" and "length"
    for x = 0 to (total area length) - length {
        for y = 0 to (total area width) - width {
            // Check this rectangle to see if any squares are taken
            Rectangle r = new Rectangle(x, y, width, length);
            if checkRectangle(area, r) returns true {
                // rectangle was fully open, add to the list
                openSpaces.Add(r);
            }
        }
    }
    return openSpaces;
}

boolean checkRectangleIsOpen(boolean[][] area, Rectangle r) {
    for i = r.x to r.width {
        for j = r.y to r.length {
            if area[i][j] is true {
                // means that this square in the rectangle is taken,
                // thus the rectangle is not fully open
                // so return false (not open)
                return false;
            }
        }
    }
    return true; // no square in the rectangle was taken
}

struct Rectangle { // just a struct to help store values
    int x, y, width, length;
}

Question

The above (pseudo)code works, but if you look at the it, it's time comes to O(n^4) :( (I think, because of four nested for loops, but I'm no expert). Plus, in the the game the total size rectangular area is 50x50, not 5x5 like my examples here. The game lags visibly whenever I do this operation.

I Googled this, but I'm not exactly sure what to call this problem. I'd really appreciate if anyone could show me an efficient algorithm to do this task. Thanks :)

  • 2
    Maybe it's worth considering other datastructures. If, for example, the objects placed on the map are not too many, you may place them in a list, so that when you place a new object you just have to do some kind of collision detection between the new object and each of the obejcts in the list. Or you simply put all occupied coordinates in a list. – TobiMcNamobi Jul 10 '14 at 20:02
  • 1
    Obviously a strength reduction would be best, but if your total size is constrained, then you might be able to make things fast enough with some parallelism. For example, representing the board with bitmasks would allow you to do things like AND, OR, XOR between all columns of two adjacent rows with a single operation. "Bitboards" are commonly used in games like chess and othello for quickly computing legal moves, etc. – Adrian McCarthy Jul 10 '14 at 23:32
  • Code review would probably be a better place for this: codereview.stackexchange.com :) – flotothemoon Aug 04 '14 at 13:17
  • @1337 Oh, hadn't heard of Code Review yet, and I thought this was more of a algorithm question, so I posted it here :) –  Aug 04 '14 at 14:36
  • @dudeprgm Funny enough, I got this is a review audit and failed it because I suggested this in the comments instead of voting it up ;) – flotothemoon Aug 04 '14 at 14:36

3 Answers3

7

Some quick thinking will show that you simply cannot do this any faster than O(NM), because there are at least that many possible outputs. You already have an O(N^2M^2) solution, so let's see if we can find a O(NM).

What I would do is, instead of finding locations that can form a rectangle of size a x b , find locations that cannot.

That is to say, something like this (pseudocode):

for each taken square:
    for each rectangle starting position:
        if rectangle at starting position includes this taken square,
            mark starting position as non-tenable

If your rectangles are small (like the 1x2 example you had above), this (efficiently implemented) is perfectly sufficient. However, for an approach that doesn't rely on the size of the rectangles for asymptotic speed . . .

Consider that (x,y) is taken. Then the rectangles that overlap with this point form a rectangle themselves, and nothing in that range can be used. So we want to mark a (discrete) range as not usable. This can be done with a 2-D Fenwick tree, which has cost O(log N log M) per update or query. To mark a range (x1, y1, x2, y2) as "in use", we:

  1. Increment (x1, y1) by one
  2. Decrement (x1, y2) by one
  3. Decrement (x2, y1) by one
  4. Increment (x2, y2) by one

So we end up doing 4 2D Fenwick tree operations per used square, of which there are at most O(NM). Then, when we're done, we check each the value of each possible starting position: if it's zero, it's a valid place to start. If it's nonzero, it cannot be used.

Total cost: O(NM log N log M). While you can likely do this faster, the speed/size ratio of implementation for this approach is extremely good (a 2-D Fenwick tree is roughly ten lines to implement).

kestrel
  • 1,314
  • 10
  • 31
  • 1
    "Cannot do this faster than O(N^2) because there are at least that many outputs." Do you mean that you can't do it faster than O(mn), where `m` and `n` are height and width of the grid, respectively? – Jim Mischel Jul 10 '14 at 19:45
  • 1
    He assumed m=n. Hint: by cleverly selecting the example (eg: 4x5 board) you can convey implicit knowledge about the task and this can pervent these kind of false assumptions. – Karoly Horvath Jul 10 '14 at 19:59
  • I kind of was assuming that `N` was the maximum of the two dimensions. Bad habit of mine, I'll fix it! Thanks. :) – kestrel Jul 10 '14 at 20:32
  • How would using this Fenwick tree answer the original question of implementing `findOpenSpaces` efficiently? – Sentient Aug 30 '23 at 21:07
1

I don't know as much about the math, but whenever I see this challenge of multiple data points inside another point... from a programmatic perspective I'm curious to try a little object.

What if rather than just an array or matrix, you program a box as a class with references to neighbor objects.
Upon startup of your game, you have a fairly busy process of building the rows with all the cross references to the neighbor boxes.

But then during play, you only are updating one box + (neighbors * defined-box-size) at a time.
If the defined-box-size is more than 2 - or you care about multiple sizes - it may still be a lot of repetitions, but significantly less than nested loops of all boxes.

When you want to find "emptiness" across the whole grid, then you will only have to call a function once on each box, as the answer has been maintained in advance as a box object state.

Mike M
  • 1,382
  • 11
  • 23
  • That's good for maintaining each grid location's "used or not" state, but not for finding contiguous blocks of available locations. – Ben Voigt Jul 10 '14 at 20:56
  • The intention is to store both... knowledge of both self and each neighbor, out to the depth that defines a block. – Mike M Jul 10 '14 at 22:37
1

Here is a solution that uses dynamic programming & set theory to reduce time complexity:-

1. Evaluate Free[i][j] which counts all free tiles in rectangle (i,j) to (n,n).
2. Rect[i][j] is whether there is rectangle (h,w) starting at (i,j).
3. Count[i][j] = Free[i][j] - Free[i][j+w] - Free[i+h][j] + Free[i+h][j+w]
4. Rect[i][j] = (Count[i][j] == h*w)

Time Complexity :-

Free[i][j] can be evaluated using DP again by:-

Free[i][j] = Row[i][j]+Col[i][j]+Free[i+1][j+1] where Row[i][j] is count of free tiles in row i from j onwards and Col[i][j] is count in col j from i onwards. Both Col & Row array can be evaluated in O(NM) using DP again. So after precomputing Col & Row you can evaluate Free[i][j] in O(NM).

Rect[i][j] can be evaluated in O(NM) after precomputing Free[i][j].

Total Complexity : O(NM)

Vikram Bhat
  • 6,106
  • 3
  • 20
  • 19