11

In a game with a 2D grid map I'm facing the following situation:

enter image description here

I need to find the largest bounding square that surrounds the player position (the red dot). Or at least a bounding square of maximum size, as there could be more. Note: square, not rectangle.

In this case, it's fairly easy to see the largest square is 8x8:

enter image description here

If I add an obstacle in the map, the largest possibility is now 5x5:

enter image description here

I am looking for a fast and efficient way of finding the (or a) maximum square that contains the player position.

What I'm currently doing is kind of brute force:

  • A 1x1 square is always possible (the player position itself).
  • Then I try all possible 2*2 squares containing the player, that's 4 possible different squares, and for each I do a 2*2 loop, checking if all grid cells are clear (not walls or obstacles).
  • If a 2*2 square is possible, then I try all possible 3*3 squares surrounding the player (that's 9 different squares) and for each I do a 3*3 loop, to check if there's no collission.
  • Et cetera, until for size N*N no square is possible.

It works, it's very straightforward, but it feels very inefficient. Obviously I'm doing a lot of redundant checks here, and I'm wondering if there is a smarter / faster way to do this. Does anyone know of an algorithm to do this efficiently?

(edit) Important addition: besides the player moving around, obstacles or walls are dynamically added or moved, so caching or precalc optimizations are somewhat hard to implement here.

Cœur
  • 37,241
  • 25
  • 195
  • 267
RocketNuts
  • 9,958
  • 11
  • 47
  • 88
  • There are various things that could be done, but depending on the maximum size of your grid, it's likely that most optimizations will be inefficient due to large initialization overhead. Is there a predefined maximum size? – Amit May 17 '15 at 21:37
  • @Amit well, the grid could be many hundreds of units horizontally and vertically, but I've noticed in pratice that the largest possible squares are rarely larger than 10, and never larger than 20. I'd say most common sizes I'm getting (for the maximum possible square) are typically 3-6. – RocketNuts May 17 '15 at 21:57
  • @Amit since you mentioned 'initialization', I added a comment about the map being somewhat dynamic. I actually thought of precalculating all sorts of bounding box information, but that doesn't really work here since the map is not fixed. – RocketNuts May 17 '15 at 22:02
  • @can you post some of your code to show how the matrix is stored? – גלעד ברקן May 18 '15 at 16:06
  • @גלעדברקן it's just an int map[] with a fixed width and height (I'm using C++ but wanted to keep this topic language independent). There are different kinds of obstacles and blocks, but basically 0 = empty and nonzero = wall or obstacle. – RocketNuts May 18 '15 at 16:48
  • @RocketNuts my solution should be quite effective handling dynamic maps as only partial calculation is required when the map changes. – Amit May 18 '15 at 18:40

4 Answers4

6

I think you can probably improve your algorithm by, at each stage, just checking on the valid boundaries of your existing 'largest square'. It is probably easier to explain this diagrammatically...but basically. All you should do is

 **Growth Algorithm**
 repeat
   search the bounding sub-squares on the valid sides of the largest valid square found so far
   increase size of square on 'valid' sides and mark newly blocked sides as invalid
 until all sides invalid

 then check if largest valid square can be translated 1 unit diagonally in any of 4 directions
 if so repeat the growth algorithm on each new square until none get bigger

By this means you should only need to test each sub-square of the final valid square once only. So a n^2 process if square is n on its side. IO don';t think you can do better as you do need to check each sub-square for validity. enter image description here

Penguino
  • 2,136
  • 1
  • 14
  • 21
  • Thanks a lot for thinking along, and the clear diagrams! I have actually been thinking of a similar 'growing edges' approach. But the problem is: the order or priority in which I validate or block my square's edges (thus deciding in which directions I can still expand) makes a difference. [1/2] – RocketNuts May 17 '15 at 23:27
  • For example, if you take the situation in my 3rd image (with the obstacle), if I expand all edges I would first find a blocked edge left, then top (same as your process so far) and after that the right and bottom edges are blocked by the obstacle, so I'd end up with a 4*4 square. Whereas the right answer is 5*5 (indicated in blue, obtained through expanding 3* left/up and 1* right/up). [2/5] – RocketNuts May 17 '15 at 23:27
  • @RocketNuts Yes I see the problem now. I don't have time to look at it in detail but possibly, when you finally get blocked in all directions (which would happen after my third diagram in your example with the extra square) you could add a step where you see if the 'blocked' maximal square can be slid diagonally in either of the 4 directions (without 'losing' your starting spot) - if so you would repeat the algorithm on each of these to see if it can expand further. That would work on your diagram 3 (at minimal extra cost), but I can't confirm if it would be valid in all cases. – Penguino May 18 '15 at 00:44
  • @RocketNuts I updated the algorithm in a sort of hand-wavy way to describe that. – Penguino May 18 '15 at 00:51
  • I would add just one thing to this approach: instead of checking in all edges one should check only on two edges in each cycle (upper and left, upper and right, down and right, down and left). Working in this way will allow you to check squares that increase only 1 unit per cycle, while if you check on all sides you can have a square with size n+2 instead of n+1. It can seem better in terms of complexity but takes a lot more effort to implement (more validity checking, etc.). Working with squares that grow by 1 unit allows also more granular checks. Let me know if something is unclear. – Gentian Kasa May 18 '15 at 12:39
1

Use a variation on this: Maximum size square sub-matrix with all 1s.

Algorithm:

Scan up, down, left and right only from the dot to generate the bounds for the 
solution matrix S, not more than 20 in each direction (that's my contribution... :)

Copy the first row and first column (within the bounds of S) from M[][] to S[][], switching 
zeros for ones and ones for zeros.

For the remaining entries (i=1 to length S, j=1 to length S) do:

If M[i][j] is 0 then
   S[i][j] = min(S[i][j-1], S[i-1][j], S[i-1][j-1]) + 1
   If S[i][j] is within distance of dot
      Best = max(S[i][j], Best)
Else
   S[i][j] = 0
Community
  • 1
  • 1
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Interesting, yes, growing the square can only occur in four diagonal directions. A problem I am having on first thought, is with every further iteration, 4 new variations occur. If I do this recursively, the numbers grow huge. But there are lots of duplicates: different order of diagonal steps which actually result in the same square, so perhaps there is a smart way to skip duplicates. – RocketNuts May 17 '15 at 23:37
  • Hmm, yes, growing happens in any of 4 diagonal directions, but the end result is a square that is simply expanded (from the player position) a specific number of units in up, right, down, and left direction. I just need to remember the number of units expaned in the 4 straight directions to identify which possibilities I already had. I'm gonna put some more thought into this! – RocketNuts May 17 '15 at 23:39
1

algorithm running

I think a video worth a thousand images. Algorithm demonstration.

In the video you can see how this algorithm find surrounding squares, unfurtunately I didn't paint the whole process It paints only good matches but I think you'll find interesting and you can notice the whole process only watching the good matches.

You can run the example of the video by yourself with processing 3, I put the whole code in this gist.

The most relevant code (written in python) is this

def checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):
    return areaX1 < 0 or areaY1 < 0 or areaX2 >= gridSize or areaY2 >= gridSize

def isAreaCompatible(type, oldAreaX1, oldAreaY1, oldAreaX2, oldAreaY2, areaX1, areaY1, areaX2, areaY2):
    global grid
    for y in range(areaY1, areaY2+1):
        for x in range(areaX1, areaX2+1):
            print "checking point (%s,%s) old area is (%s,%s) (%s,%s) new area is (%s,%s) (%s,%s)" % (x,y,oldAreaX1, oldAreaY1, oldAreaX2, oldAreaY2, areaX1,areaY1,areaX2,areaY2)    
            if x >= oldAreaX1 and x <= oldAreaX2 and y >= oldAreaY1 and y <= oldAreaY2: 
                print "This area belongs to previous area, won't check"
            else:         
                if grid[y][x].type != type: print "false"; print "This area have a different color/type so it's not compatible"; return False;
    return True;

def explore(type, x1, y1, x2, y2):
    #Right and bottom
    print "----- Right and bottom ------"
    areaX1 = x1;
    areaY1 = y1;
    areaX2 = x2+1;
    areaY2 = y2+1;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):      
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Bottom and left    
    print "----- Bottom and left ------"
    areaX1 = x1-1;
    areaY1 = y1;
    areaX2 = x2;
    areaY2 = y2+1;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2): 
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Left and top
    print "----- Left and top ------"
    areaX1 = x1-1;
    areaY1 = y1-1;
    areaX2 = x2;
    areaY2 = y2;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):     
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);

    #Top and right
    print "----- Top and right ------"
    areaX1 = x1;
    areaY1 = y1-1;
    areaX2 = x2+1;
    areaY2 = y2;
    if not checkOutBoundaries(areaX1, areaY1, areaX2, areaY2):     
        if isAreaCompatible(type, x1, y1, x2, y2, areaX1, areaY1, areaX2, areaY2):
            addAnim(areaX1, areaY1, areaX2, areaY2);
            addMatch(areaX1, areaY1, areaX2, areaY2);
            explore(type, areaX1, areaY1, areaX2, areaY2);
ZooMMX
  • 838
  • 9
  • 14
0

Here's a completely different approach:

You hold a a 2d matrix of size [Length, 2] Length being the size of your map (I'm assuming symmetric width / height, but if not just pick an axis). I'll call the 1st rank (Length) column (you could rotate the entire algorithm 90 degrees and call this row). Each column holds 2 values - Min Position & Max Position of valid range in the column.

You fill this matrix with the following algorithm:

Start with the dot column, find & store the min & max position of the contiguous range above & below the dot. If dot is at x,y:

int colMin, colMax;
for(colMin = y; colMin > 0;) {
   if(Matrix[x, colMin - 1].IsValid)
       colMin--;
   else break;
}
for(colMax = y; colMax < maxHeight;) {
   if(Matrix[x, colMax + 1].IsValid)
       colMax++;
   else break;
}
MinMaxValid[x, 0] = colMin;
MinMaxValid[x, 1] = colMax;

You proceed in both directions independently with the following algorithm:

int minCol = 0, maxCol = maxWidth; // These hold the min/max range of valid columns
for(int col = x - 1; col >= 0; col--) {
   for(colMin = y; colMin > MinMaxValid[col + 1, 0];) { // Cell is only valid if it overlaps to the next range
      if(Matrix[col, colMin - 1].IsValid)
          colMin--;
      else break;
   }
   for(colMax = y; colMax < MinMaxValid[col + 1, 1];) { // Cell is only valid if it overlaps to the next range
      if(Matrix[col, colMax + 1].IsValid)
          colMax++;
      else break;
   }
   if((colMax - colMin) >= (x - col)) { // if the found range is smaller than the distance for x, it can't be a part of the square
      MinMaxValid[col, 0] = colMin;
      MinMaxValid[col, 1] = colMax;
   }
   else {
      minCol = col + 1; 
      break; // We're done on this side
   }
}

for(int col = x + 1; col < maxWidth; col++) {
   for(colMin = y; colMin > MinMaxValid[col - 1, 0];) { // Cell is only valid if it overlaps to the previous range
      if(Matrix[col, colMin - 1].IsValid)
          colMin--;
      else break;
   }
   for(colMax = y; colMax < MinMaxValid[col - 1, 1];) { // Cell is only valid if it overlaps to the previous range
      if(Matrix[col, colMax + 1].IsValid)
          colMax++;
      else break;
   }
   if((colMax - colMin) >= (col - x)) { // if the found range is smaller than the distance for x, it can't be a part of the square
      MinMaxValid[col, 0] = colMin;
      MinMaxValid[col, 1] = colMax;
   }
   else {
      maxCol = col - 1;
      break; // We're done on this side
   }
}

You now have your MinMaxValid filled. The next part is to run over the rows and find the largest square:

int maxSquareSize = 0, maxSquareCol = minCol;
for(int col = minCol; (MinMaxValid[col, 1] - MinMaxValid[col, 0]) >= (maxCol - col); col++) {
   for(int squareSize = MinMaxValid[col, 1] - MinMaxValid[col, 0] + 1; squareSize > maxSquareSize; squareSize--) {
      if((Min(MinMaxValid[col, 1], MinMaxValid[col + squareSize - 1, 1]) - Max(MinMaxValid[col, 0], MinMaxValid[col + squareSize - 1, 0])) >= (squareSize) {
         maxSquareSize = squareSize;
         maxSquareCol = col;
         break;
      }
   }
}

The last part works like this:

Any column can participate in a square only as large as it's height. In order to do so, the union of the current column range, and the range at col + squareSize - 1 must be at least squareSize tall.

Now comes the best part! Whenever the scene changes (A square becomes invalid, the dot moves, etc...) you can only invalidate a certain range of your MinMaxValid matrix and re-evaluate as necessary. I didn't include this logic as this answer is long enough, but that's the easy part (Basically, you reduce the column range to fit the new scenario, and rescan both directions again).

I must say I haven't tried this one out, but even if it doesn't work (And please let me know so I could remove this comment :-), it's got to be close to a valid solution.

Amit
  • 45,440
  • 9
  • 78
  • 110
  • Thanks a lot Amit, for your extensive answer. I didn't get the time to fully grasp it yet, I'm going to analyze it in further detail and let you know! – RocketNuts May 18 '15 at 21:49