So I have an assignment where I have to recreate a 3d chessboard that is a RxC grid of squares each being a different height. If the chessboard is water tight, and someone pours
This is going to take more than simply calculating volume.
According to the examples you posted, you have test for containment first, then worry about the volume of the container.
I suggest determining whether or not there is a closed polygon in the board.
If the polygon is closed, determine its area.
The height used for volume calculation will be the minimum height of all the bounding walls.
Finally, the volume will be the minimal height multiplied by the area of the polygon.
Research on the web for algorithms on determining a closed polygon based on a vector of vertices or line segments.
Fun question, with many varied solutions. I've been thinking about it this afternoon and I would go for something like flood-fill with a priority queue (min-heap, perhaps). Let's call it the fence
.
You'll also want to keep track of which items have been visited. Mark all items as unvisited, initially.
Start off by adding all points around the perimeter of your grid to the fence
.
Now you loop like so:
Pop the front item from the fence
. You have selected one of the lowest points on the perimeter.
You now do a flood-fill from that point. You can do this recursively (depth-first), but I will discuss this using an unordered queue (breadth-first). Let's call this queue the flood
. You start by pushing the item onto flood
.
Flooding then goes like this: Loop until there are no items remaining in flood
...
flood
flood
.fence
. You'll want to have a way to tell whether the item is already in fence
- you don't want to add it again. Maybe you can extend your 'visited' flags to cope with this.And that's it. Admittedly it was just a thought experiment while I lay around feeling hungover and seedy, but I reckon it's good.
As you requested... Some pseudocode.
Initial setup:
## Clear flags. Note I've added a 'flooding' flag
for each item in map
item.visited <- false # true means item has had its water depth added
item.fenced <- false # true means item is in the fence queue
item.flooding <- false # true means item is in the flooding queue
end
## Set up perimeter
for each item on edge of map (top, left, right, bottom)
push item onto fence
end
waterlevel <- 0
total <- 0
Now the main chunk of the algorithm
while fence has items
item <- pop item from fence
if item.visited = true then loop again
## Update water level
if item.height > waterlevel then waterlevel = item.height
## Flood-fill item using current water level
push item onto flood
item.flooding <- true
while flood has items
item <- pop item from flood
depth <- waterlevel - item.height
if depth >= 0 then
# Item is at or below water level. Add its depth to total.
total <- total + depth
item.visited <- true
# Consider all immediate neighbours of item.
for each neighbour of item
if neighbour.visited = false then
if neighbour.flooding = false then
push neighbour onto flood
neighbour.flooding <- true
end
end
end
else
# Item is above water
item.flooding <- false
if item.fenced = false then
push item onto fence
item.fenced <- true
end
end
end
end
This is a bit brute-force-ish but would probably work.
You might try to conceptually break the board into layers for instance:
-------------------------
0 | 1 | 1 | 0 | 1 | 1 | 0
1 | 0 |-1 | 1 | 0 | 0 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1
-------------------------
Looking at just the lowest layer. Assuming -1 is the bottom, the board would look like this:
-------------------------
0 | 0 | 0 | 0 | 0 | 0 | 0
0 | 0 |-1 | 0 | 0 | 0 | 0
0 | 0 | 0 | 0 | 0 | 0 | 0
-------------------------
For each square, determine if there exists a square to the left, right, top and bottom that all have a greater value. In this case we count 1.
Then move to the next layer, filling in the "holes" in the last layer.
-------------------------
0 | 1 | 1 | 0 | 1 | 1 | 0
1 | 0 | 0 | 1 | 0 | 0 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1
-------------------------
Rinse, repeat. In this layer we count 4 giving us a total of 5.
-------------------------
1 | 1 | 1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 1 | 1 | 1 | 1
-------------------------
Top layer obviously has none and we're done.
In pseudo code:
for each layer l in layers
for each square in l
if there exists a square left, right, top and bottom with higher value
count the square.
Edit
Of course since there is something seriously wrong with me, when I woke up this morning the first thing I thought of was this problem and immediately broke my solution.
Let's make one change to the example:
-------------------------
0 | 1 | 1 | 0 | 1 | 1 | 0
1 | 0 |-1 | 1 | 0 | 0 | 1
1 | 1 | 1 | 1 | 1 | 0 | 1
-------------------------
^
Open a hole to the outside. With the current algorithm we would get a solution of 4 which is obviously wrong.
To fix this we need to implement a back tracking algorithm.
Instead of looking anywhere to the left, right, top and bottom for a higher value, we check just the immediately adjacent squares. If any are at the same height we need to visit that square as well and perform the check again. If we find our way to the outside then the original square (and subsequently all the visited squares) fail. If we hit a dead end, then all the visited squares can be counted.
With this modification we would get the correct result of 3.
Here is a piece of working code: the basic idea is slicing the board horizontally into levels, and determining the volume each level can hold (complexity O(x * y * z)):
#include <stdio.h>
#include <memory.h>
// The Cell structure for each level
struct Cell
{
unsigned char height; // either 0 or 1
bool visited; // we only visit the cells that have height of 0
};
// The recursive function that visits a cell, accumulate the water volume (or if leaked due to being at the border, reset the values); it also
// looks to its 4 adjacent cells (if valid) recursively.
// Note that the top level function actually attempts to visit *all* the "connected" cells (and mark them as visited, so they will not be visited again)
// From the top level, the cells are thus visited in "chunks" (as long as they are connected)
void VisitCell(Cell* one_level_cells, unsigned short a_w, unsigned short a_h, unsigned short w, unsigned short h, unsigned __int64* sum, bool* leaked)
{
Cell& cell = one_level_cells[h * a_w + w];
if (cell.height == 0 && !cell.visited)
{
cell.visited = true;
if (w == 0 || w + 1 == a_w || h == 0 || h + 1 == a_h)
{
// I am at the border while I am not guarding the water, the water for this "chunk" is then leaked!
*leaked = true;
*sum = 0;
}
if (!*leaked)
{
// potentially increment the volume, until it's detected leaked at some point
++*sum;
}
if (w < a_w - 1) VisitCell(one_level_cells, a_w, a_h, w+1, h, sum, leaked);
if (w > 0) VisitCell(one_level_cells, a_w, a_h, w-1, h, sum, leaked);
if (h < a_h - 1) VisitCell(one_level_cells, a_w, a_h, w, h+1, sum, leaked);
if (h > 0) VisitCell(one_level_cells, a_w, a_h, w, h-1, sum, leaked);
}
}
//@param int const * const unsigned short *a_board - argument representing the NxM board.
//@param unsigned short a_w - argument representing the width of the board
//@param unsigned short a_h - argument representing the height of the board
//@return - unsigned __int64 - the volume of water the board retains
// complexity: O(a_w * a_h * max_height)
unsigned __int64 CalculateVolume(const unsigned short *a_board, unsigned short a_w, unsigned short a_h)
{
if (!a_board || a_w < 3 || a_h < 3)
{
return 0;
}
// Basic algorithm: slice the board horizontally into as many levels as the maximum height of the board
// for each sliced level, determine the water volume cubed so far, and the total volume is the sum of the volume of the individual level
unsigned __int32 n = a_w * a_h;
unsigned short max_height = 0;
for (unsigned __int32 i = 0; i < n; ++i)
{
if (max_height < a_board[i])
{
max_height = a_board[i];
}
}
unsigned short *board = new unsigned short[n];
memcpy(board, a_board, n * sizeof(unsigned short));
Cell* one_level_cells = new Cell[n];
unsigned __int64 total_volume = 0;
for (unsigned short i = 0; i < max_height; ++i)
{
// form a new current level of cells (and update the copy of the board accordingly)
unsigned __int64 volume_this_level = 0;
for (unsigned __int32 j = 0; j < n; ++j)
{
if (board[j] > 0)
{
--board[j];
one_level_cells[j].height = 1;
}
else
{
one_level_cells[j].height = 0;
}
one_level_cells[j].visited = false;
}
// visit all the cells within the current level
// we mark the cells after being visited, and the cells are visited in "chunks" when they are "connected" together
// so effectively, most of the top level cell visiting would return immediately, rather than trying to revisit the cells again and again
for (unsigned short h = 0; h < a_h; ++h)
{
for (unsigned short w = 0; w < a_w; ++w)
{
unsigned __int64 sum = 0;
bool leaked = false;
// NB: the top level function here will attempt to cover *all* the connected cells at the current level (in the recursion)
// so even though we are still iterating through all the cells at the top level, most of them should find that the cell has been visited
// so the sum here is actually a "chunked" sum in the perception of the top level cells
VisitCell(one_level_cells, a_w, a_h, w, h, &sum, &leaked);
volume_this_level += sum;
}
}
total_volume += volume_this_level;
}
delete[] one_level_cells;
delete[] board;
return total_volume;
}
int main()
{
// feel free to play with this board
unsigned short board[] = {
2, 2, 2, 2, 2, 2, 2, 2,
2, 1, 1, 1, 1, 1, 1, 2,
2, 1, 2, 3, 3, 2, 1, 2,
2, 1, 3, 1, 1, 3, 1, 2,
2, 1, 3, 1, 1, 3, 1, 2,
2, 1, 2, 3, 3, 2, 1, 2,
2, 1, 1, 1, 1, 1, 1, 2,
2, 2, 2, 2, 2, 2, 2, 2,
};
printf("Volume: %lld\n", CalculateVolume(board, 8, 8));
return 0;
}
This is python(2.7) version of the pseudocode by @paddy .Hope this will help someone .
import heapq
block =[
1, 2, 3,
4 ,2,6,
7, 0,5,
11,15, 13,
14,15,16,
1,0,1,
1,1,1]
cmax =3;
rmax =7;
items =[]
fence = []
flood = []
waterlevel = 0
total = 0
class Item(object):
visited = False
fenced = False
flooding = False
height= 0
index = 0
i=0
## initializing blocks
for val in block:
item = Item();
item.height = val
item.index = i
i+=1
items.append((item.height,item))
## find out the edges
for i in range (cmax):
heapq.heappush(fence, items[i])
heapq.heappush(fence, items[i+(rmax-1)*cmax])
print items[i][1].height,items[i+(rmax-1)*cmax][1].height
for i in range(1,rmax-1):
heapq.heappush(fence, items[cmax*i])
heapq.heappush(fence, items[cmax*i+(cmax-1)])
print items[cmax*i][1].height,items[cmax*i+(cmax-1)][1].height
## get neighbour
def get_neighbour(i):
c= i%cmax
r= i//cmax
neighbour = []
if (c != 0):
neighbour.append(items[r*cmax+c-1])
if (c != (cmax -1)):
neighbour.append(items[r*cmax+c+1])
if (r != 0):
neighbour.append(items[(r-1)*cmax+c])
if (r != (rmax -1)):
neighbour.append(items[(r+1)*cmax+c])
return neighbour
while (len(fence)>0):
item = heapq.heappop(fence)
if(item[1].visited):
continue
if (item[1].height > waterlevel):
waterlevel = item[1].height
heapq.heappush(flood,item)
item[1].flooding = True
while(len(flood)>0):
fitem = heapq.heappop(flood)
depth = waterlevel - fitem[1].height
if (depth >= 0):
total += depth
fitem[1].visited = True
neighbour = get_neighbour(fitem[1].index)
for nitem in neighbour:
if nitem[1].visited == False :
if nitem[1].flooding == False :
heapq.heappush(flood,nitem)
nitem[1].flooding = True
else:
fitem[1].flooding = False
if fitem[1].fenced == False:
heapq.heappush(fence,fitem)
fitem[1].fenced = True
print total