7

I'm working on a specific layout algorithm to display photos in a unit based grid. The desired behaviour is to have every photo placed in the next available space line by line.

animation of what the algorithm should achieve

Since there could easily be a thousand photos whose positions need to be calculated at once, efficiency is very important.

Has this problem maybe been solved with an existing algorithm already? If not, how can I approach it to be as efficient as possible?

Edit Regarding the positioning: What I'm basically doing right now is iterating every line of the grid cell by cell until I find room to fit the element. That's why 4 is placed next to 2.

Carl Manaster
  • 39,912
  • 17
  • 102
  • 155
matteok
  • 2,189
  • 3
  • 30
  • 54
  • 1
    We need to know more about the constraints of the problem. 1) Is the grid size fixed for any problem, or can you decide to grow the grid (horizontally? vertically?) after you see the photo sizes? 2) How are we defining "top-most and left-most"? It sounds like you have two objectives you are simultaneously optimizing for here. Is it the case that there is a "bottom-ness" score and a "right-ness score" and you are trying to minimize the maximum of those two? – Jerry Federspiel Jul 06 '15 at 14:38
  • Why doesn't 4 go below 3, flush to the left? – Scott Hunter Jul 06 '15 at 14:39
  • I edited my post to try and clarify the desired behavior @Jerry the grid has a fixed width and a dynamic height – matteok Jul 06 '15 at 14:47
  • Do you have access to the entire set of photos at once, or can you only consider one photo at a time? Are there any constraints on the sizes of the photos, or any knowledge of how those sizes are distributed? – Jerry Federspiel Jul 06 '15 at 15:30
  • I have access to all photos at once and the whole layout needs to be calculated when that happens. For each photo I calculate the necessary units it should occupy and end with something like this: [{w:1,h:1},{w:2,h:2},{w:3,h:1}...] – matteok Jul 06 '15 at 15:32

3 Answers3

3

How about keeping a list of next available row by width? Initially the next-available-row list looks like:

(0,0,0,0,0)

When you've added the first photo, it looks like

(0,0,0,0,1)

Then

(0,0,0,2,2)

Then

(0,0,0,3,3)

Then

(1,1,1,4,4)

And the final photo doesn't change the list.

This could be efficient because you're only maintaining a small list, updating a little bit at each iteration (versus searching the entire space every time. It gets a little complicated - there could be a situation (with a tall photo) where the nominal next available row doesn't work, and then you could default to the existing approach. But overall I think this should save a fair amount of time, at the cost of a little added complexity.

Update In response to @matteok's request for a coordinateForPhoto(width, height) method:

Let's say I called that array "nextAvailableRowByWidth".

public Coordinate coordinateForPhoto(width, height) {
    int rowIndex = nextAvailableRowByWidth[width + 1]; // because arrays are zero-indexed
    int[] row = space[rowIndex]
    int column = findConsecutiveEmptySpace(width, row);
    for (int i = 1; i < height; i++) {
        if (!consecutiveEmptySpaceExists(width, space[i], column)) {
            return null;
            // return and fall back on the slow method, starting at rowIndex
        }
    }
    // now either you broke out and are solving some other way,
    // or your starting point is rowIndex, column.  Done.
    return new Coordinate(rowIndex, column);
}

Update #2 In response to @matteok's request for how to update the nextAvailableRowByWidth array:

OK, so you've just placed a new photo of height H and width W at row R. Any elements in the array which are less than R don't change (because this change didn't affect their row, so if there were 3 consecutive spaces available in the row before placing the photo, there are still 3 consecutive spaces available in it after). Every element which is in the range (R, R+H) needs to be checked, because it might have been affected. Let's postulate a method maxConsecutiveBlocksInRow() - because that's easy to write, right?

public void updateAvailableAfterPlacing(int W, int H, int R) {
    for (int i = 0; i < nextAvailableRowByWidth.length; i++) {
        if (nextAvailableRowByWidth[i] < R) {
            continue;
        }
        int r = R;
        while (maxConsecutiveBlocksInRow(r) < i + 1) {
            r++;
        }
        nextAvailableRowByWidth[i] = r;
    }
}

I think that should do it.

Carl Manaster
  • 39,912
  • 17
  • 102
  • 155
  • I'm not sure what you mean by next available row by width. Could you explain that a little further? – matteok Jul 06 '15 at 18:22
  • You only have (in this example) 5 possible widths. At the start, any width can go on the 0 row. After placing a 1x1 photo, widths 1-4 can still go onto the 0 row, but a 5-wide photo would have to go onto the 1 row. – Carl Manaster Jul 06 '15 at 18:36
  • ahhhhh I just got it but it took me a while :) This only applies if a photo is exactly 1 unit tall right? Unfortunately many of the pictures will be taller than they are wide. Can you think of a variation of your solution that would take the height into consideration as well? – matteok Jul 06 '15 at 18:51
  • Yes, but it increases the complexity significantly. Most of the time (depends on distribution of photo heights and widths), this will put your pointer at the right row. When it doesn't, it will put you pretty close to the right row. So I suspect this is the biggest bang-for-the-buck in terms of adding minimal complexity for maximum gain. If you have a maximum height, that constraint helps. – Carl Manaster Jul 06 '15 at 19:04
  • Alright I'll go with this version. I really like your approach. How would you implement a coordinateForPhoto(width, height) method that updates the next row array? – matteok Jul 06 '15 at 20:54
  • Thank you for your help I appreciate it – matteok Jul 06 '15 at 23:50
  • I was actually hoping you could show me a solution how I can populate the availableRowByWidth array – matteok Jul 07 '15 at 07:57
1

How about a matrix (your example would be 5x9) where each cell has a value representing the distance from the top left corner (for instance (row+1)*(column+1) [+1 is only necessary if your first row and value are 0]). In this matrix you look for the area which has the lowest value (when summing up the values of empty cells). A 2nd matrix (or a 3rd dimension of the first matrix) stores the status of each cell.

edit:

int[][] grid = new int[9][5];
int[] filledRows = new int [9];
int photowidth = 2;
int photoheight = 1;
int emptyRowCounter = 0;
boolean photoFits = true;

for(int i = 0; i < grid.length; i++){
    for(int m = 0; m < filledRows.length; m++){
        if(filledRows[m]-(photoHeight-1) > i || filledRows[m]+(photoHeight-1) < i){
            for(int j = 0; j < grid[i].length; j++){
                if(grid[i][j] == 0){
                    for(int k = 0; k < photowidth; k++){
                        for(int l = 0; k < photoheight){
                            if(grid[i+l][j+k]!=0){
                                photoFits = false;
                            }
                        }
                    }
                } else{
                    emptyRowCounter++;
                }
            }
            if(photoFits){
                //place Photo at i,j
            }
            if(emptyRowCounter == 5){
                filledRows[i] = 1;
            }
        }
    }
}
tobyUCT
  • 137
  • 9
  • I think I may have miscommunicated my desired outcome by saying "left most" and "top most". I edited my question to illustrate what I really meant – matteok Jul 06 '15 at 15:01
  • 1
    Then you would not need the matrix representing the distance from top left, just one with information whether a cell is empty or not. This matrix should then be searched for an empty cell. when you find an empty cell you check if the photo would fit. if [1,1] is empty and photo has size [1,3] you need to check [1,2] and [1,3] aswell. – tobyUCT Jul 06 '15 at 15:07
  • Is there a more efficient way then just checking every cell for availability and if it's available check if the space is enough? This results in so many checks for each added item especially when the matrix has grown to 5 * 1000 for example – matteok Jul 06 '15 at 15:09
  • 1
    You could create another array of rows which are not completely filled (yet). If a row is completely filled you don't need to check it anymore. if a photo is 3 slots high you don't need to check the 2 rows before and after completely filled rows either because it will never fit. Note: this only works if your grid never extends the columns. – tobyUCT Jul 06 '15 at 15:19
  • Could you provide some example code when and how you would implement this? – matteok Jul 06 '15 at 15:25
  • you can do that but i think it will never have an effect. (I could be mistaken though). the first part should calculate with (photoHeight - 1) though, I'll edit that. – tobyUCT Jul 06 '15 at 16:07
  • isn't filledRows.length and grid[i].length both always 9? – matteok Jul 06 '15 at 16:15
  • So far you talked about resizing the matrix, so it wouldn't always be 9. But both arrays should always have the same length. That is correct – tobyUCT Jul 07 '15 at 05:49
0

In the gif you have above, it turned out nicely that there was a photo (5) that could fit into the gap under (1) and to the left of (2). My intuition suggests we want to avoid creating gaps like that. Here is an idea that should avoid these gaps.

Maintain a list of "open regions", where an open region has a int leftBoundary, an int topBoundary, and an optional int bottomBoundary. The first open region is just the whole grid (leftBoundary:0, topBoundary: 0, bottom: null).

Sort the photos by height, breaking ties by width.

Until you have placed all photos:

Choose the tallest photo (in case of ties, choose the widest of the tallest photos). Find the first open region it can fit in (such that grid.Width - region.leftBoundary >= photo.Width). Place the photo at the top left of this region. When you place this photo, it may span the entire width or height of the region.

  1. If it spans both the width and the height of the region, the region is filled! Remove this region from the list of open regions.

  2. If it spans the width, but not the height, add the photo's height to the topBoundary of the region.

  3. If it spans the height, but not the width, add the photo's width to the leftBoundary of the region.

  4. If it does not span the height or width of the boundary, we are going to conceptually divide this region into two: one region will cover the space directly to the right of this photo (call it rightRegion), and the other region will cover the space below this region (call it belowRegion).

    rightRegion = { leftBoundary = parentRegion.leftBoundary + photo.width, topBoundary = parentRegion.topBoundary, bottomBoundary = parentRegion.topBoundary + photo.height }

    belowRegion = { leftBoundary = 0, topBoundary = parentRegion.topBoundary + photo.height, bottomBoundary = parentRegion.bottomBoundary }

    Replace the current region in the list of open regions with rightRegion, and insert belowRegion directly after rightRegion.


You can visualize how this algorithm would work on your example: First, it would sort the photos: (2,3,4,1,5).

It considers 2, which fits into the first region (the whole grid). When it places 2 at the top left, it splits that region into the space directly to the right of 2, and the space below 2.

Then, it considers 3. It considers the open regions in turn. The first open region is to the right of 2. 3 fits there, so that's where it goes. It spans the width of the region, so the region's topBoundary gets adjusted downward.

Then, it considers 4. It again fits in the first open region, so it places 4 there. 4 spans the height of the region, so the region's leftBoundary gets adjusted rightward.

Then, 1 gets put in the 1x1 gap to the right of 4, filling its region. Finally, 5 gets put just below 2.

Jerry Federspiel
  • 1,504
  • 10
  • 14
  • Thank you for your answer! I read about a similar algorithm to solve packaging problems. Unfortunately if I'm not mistaken this will completely break the order of the photos correct? The order should be maintained as good as possible. I know that this layout will produce gaps in some cases but after careful evaluation the advantages still overweigh the disadvantage of that possibility. – matteok Jul 06 '15 at 17:40
  • Yes, this disregards the original order of the photos. Is it the case that the *placement* of the photos is not in question, but specifically we want to know how to arrive at it in the fastest way possible? Or do we have some freedom (for example) to place 1 on the far left, 2 on the far right, and others in between? – Jerry Federspiel Jul 06 '15 at 17:51
  • The placement should work as depicted in the animation. I have already implemented a brute force version of this which looks great but performs poorly. – matteok Jul 06 '15 at 17:52