2D Waterflow Problem

Here is the map of a two-dimensional hill station where each digit indicates the height of the corresponding position. How much water accumulates in the hill after the rainfall?

Details and Assumptions:

  • The given map has dimensions 100 × 100. 100 \times 100.
  • Water flows off edges of the map. For example, the following map cannot contain any water in the corner even though it is lower than the other ones:
1
2
3
221
222
222

  • Water cannot flow off diagonals. For example, although all the corners of the map below are lower, water does not flow off them. The amount of water collected in this map is 2.
1
2
3
121
202
121

  • You can check out the one-dimensional version here .


The answer is 14118.

This section requires Javascript.
You are seeing this because something didn't load right. We suggest you, (a) try refreshing the page, (b) enabling javascript if it is disabled on your browser and, finally, (c) loading the non-javascript version of this page . We're sorry about the hassle.

3 solutions

Darryl Dennis
Sep 20, 2018
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# additional lists included to allow the output data for water depth and standing water elevations to be imported to an Excel sheet.
# additional lists are srickly for interest and not required for the solution. 

w, h = 100, 100;
hill_elv = [[]]
hill_elv = [[0 for x in range(w)] for y in range (h)]; # array to hold hill elevations from file
drain_elv = [[]]
drain_elv = [[9 for x in range(w)] for y in range (h)]; #array to hold water drain elevations for calculation.  fill list with high elv
water_depth =[[]]
water_depth =[[0 for x in range(w)] for y in range (h)]; #not required- included to produce Excel chart indicating depth of water remaining on the hill
water_elv =[[]]
water_elv =[[0 for x in range(w)] for y in range (h)];  # not required -included to produce Excel chart indicating water levels rmaing on the hill
rline = 0
rchr = 0
chg = True

with open("J:\Brilliant\\rain on hill\\rain on hill bril.txt") as file: # read from file fill python list of lists with hill elevation data
    for line in file:
        rchr = 0
        for chr in line[:100]:
            hill_elv[rline][rchr]= int(chr)
            rchr += 1
        rline += 1

file.close()
for x in range(w):    # fill outside edge cells with starting drain level elevations
    for y in range(h):
        if x == 0 or x == 99 or y == 0 or y == 99:
            drain_elv[x][y]= hill_elv[x][y]

while chg == True:
        chg = False
        for row in range(1,99):
            for col in range(1,99): #increment through all the cells on the hill
                north = max(drain_elv[row-1][col],hill_elv[row-1][col])  # find current drain level to each boarder cell on hill in this pass
                south = max(drain_elv[row+1][col],hill_elv[row+1][col])
                west = max(drain_elv[row][col-1],hill_elv[row][col-1])
                east = max(drain_elv[row][col+1],hill_elv[row][col+1])
                if min(north,south,east,west) != drain_elv[row][col]:  # determine if chage is to be made in this pass
                    drain_elv[row][col] =  min(north,south,east,west)  # make change to drain elevation list
                    chg = True

hold_count =0    
for row in range(1,99):
    for col in range(1,99):   #increment through all the cells on the hill
        if drain_elv [row][col] > hill_elv [row][col]: # if water elevation is above hill elevation in this cell
            hold_count += drain_elv [row][col]-hill_elv [row][col] # add standing water volume to total
            water_depth[row][col] = drain_elv [row][col]-hill_elv [row][col]   #not required -    calculate and store water depth for this call 
        if drain_elv [row][col] > hill_elv [row][col]:   #not required- --   for water elevations above hill elevations
            water_elv[row][col] = drain_elv [row][col]   #not required---    store standing water elevation for this cell
        else:
            water_elv[row][col] = -1  #not required-      if hill elevation is not below water store -1 


print("Total water volume held on the hill is ", hold_count," units")

import csv
csvfile = "J:\\Brilliant\\rain on hill\\water elevation.txt"

with open(csvfile, "w") as output: # write water level elevation list to file for use with Excel chart 
    writer = csv.writer(output, lineterminator='\n')
    writer.writerows(water_elv)
csvfile = "J:\\Brilliant\\rain on hill\\water depth.txt"

with open(csvfile, "w") as output: # write depth list to file for use with Excel chart 
    writer = csv.writer(output, lineterminator='\n')
    writer.writerows(water_depth)    

graphic chart indicating the depth of the standing water on the hill.

Red indicates no water on cell.

the darker the color the deeper the water is on each cell of the hill.

graphic chart indicating the elevation level of the standing water on the hill.

The dark green indicates no water ( hill is exposed)

The lighter the color the higher the elevation of the standing water on the hill.

Charts where made using conditional formatting in an Excel sheet. I thought someone else may be interested to see a graphical image of the water standing on the hill. These grafts have no value other then my curiosity, interest, and amusement in getting a real world feel for the solution.

I posted this as a different approach to the solution. More eloquent solutions have bean posted. Considering I am a newby to Pyton just getting started looking through a couple of online tutorials and examples a few days ago, I am satisfied with the results. This was my first attempt to do anything in Python.

Mykyta Shvets
Jul 12, 2018

Previous solution

I like when the code is short, so there was a time-efficient solution to the problem. The key is to check the map layer by layer, and find any areas that are not connected to the edges. You can find the previous solution here . See an improved solution in the next section.


An improved solution

This solution can use fractions as tile-heights, as well as huge positive and negative values.

  • Its run time does not depend on specific heights. However, the broader the range the more time you need to calculate.
  • 1d maps [0, 1, 2, 3] and [-1000, 999, 0.435, + \infty ] would take the same time to calculate.
  • [0, 1, 2, 3, 0, 1, 2, 3] would take twice the time, but [0, 1, 2, 3, 4, 5, 6, 7] would be four times longer.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from functools import reduce
import math

# Open the given map from a txt file
with open('hillmap.txt') as f:
    lines = f.readlines()
    # Create a 2d list of heights
    hillmap = [[int(ch) for ch in line[:] if ch != "\n"] for line in lines]
width, height = len(hillmap), len(hillmap[0])

# The total water amount
watertotal = 0
# Set of hill layers
layers = reduce(lambda a, b: a | b, [{n for n in row} for row in hillmap])
layers = sorted(list(layers))

# Move upwards layer by layer
for k, layer in zip(range(len(layers)), layers):
    # A map of whether there is a hill or not (1 Hill, 0 Water, -1 Empty)
    temp_map = [[n >= layer for n in row] for row in hillmap]
    # Initialize a set of cells connected to the edge directly
    cells_to_check = \
        {(x, 0) for x in range(0, width)
         if not temp_map[x][0]} |\
        {(0, y) for y in range(0, height)
         if not temp_map[0][y]} | \
        {(x, height - 1) for x in range(0, width)
         if not temp_map[x][height - 1]} | \
        {(width - 1, y) for y in range(0, height)
         if not temp_map[width - 1][y]}
    # While there are unprocessed cells left
    while cells_to_check:
        cx, cy = cells_to_check.pop()
        # Mark this cell Empty
        temp_map[cx][cy] = -1
        # Check all adjacent cells
        for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
            # If it is Water and it's within the map bounds
            if cx + dx < width and cx + dx >= 0 and \
                    cy + dy < height and cy + dy >= 0 and \
                    temp_map[cx + dx][cy + dy] == 0:
                # Then add this cell to the set of cell to be emptied.
                cells_to_check.add((cx + dx, cy + dy))
    # Get volume by multiplying the area by the difference between layers
    layertotal = sum([sum([r == 0 for r in row]) for row in temp_map]) * \
        max(0, layers[k] - layers[k - 1])
    # Assume inf * 0 = 0 (we're talking about physical volume after all)
    if not math.isnan(layertotal):
        watertotal += layertotal

# Print the output
print(watertotal)

1
2
14118
[Finished in 0.4s]

Haha, there are no caves, if there is a hill of height H, then it is a solid hill of height H all the way down.

What is the complexity of your solution? linear in the number of squares?

Agnishom Chattopadhyay - 2 years, 11 months ago

Log in to reply

Yeah, in the worst case scenario the algorithm checks every square in every layer, that is (width * height * depth) squares. The depth in here is the difference of the highest hill and the lowest valley (9 for that problem)

Mykyta Shvets - 2 years, 11 months ago

Log in to reply

Can there be an algorithm whose complexity is independent of the depth?

Agnishom Chattopadhyay - 2 years, 11 months ago

Log in to reply

@Agnishom Chattopadhyay I think I have an idea how it could be done! I'll post my results once I try it out.

Mykyta Shvets - 2 years, 11 months ago

Log in to reply

@Mykyta Shvets Great, I look forward to seeing your answer

Agnishom Chattopadhyay - 2 years, 11 months ago

Log in to reply

@Agnishom Chattopadhyay Uhm, it's kinda up here. It won't check all layers 0 through 100 if you have a map with spikes like [0, 100, 0, 50]. And it now can handle positive and negative infinity tiles!

Mykyta Shvets - 2 years, 11 months ago

I wonder how the 3d version of this problem will look like!

Mehdi K. - 2 years, 10 months ago
Calvin Osborne
Jul 8, 2018

This is the second part for my solution, the first being my original approach and code which can be found below. Since when I first wrote this solution, I have rewritten parts of my code using a much more efficient approach. Inspired by a comment by Agnishom Chattopadhyay, my improved program uses a psuedo-breadth first style approach, which works the following way:

  1. Create an array (queue) which will stores the positions of "air" that hasn't yet been checked
  2. Create a list of integers (shouldQueueArray) which will store whether positions can be added to the queue (positions won't be added if they are "out of bounds", already been checked or added to the queue, or are part of the hill)
  3. Fill shouldQueueArray with -1 at all the hill positions and 0 everywhere else. For the bottom most edge blocks of water load them into the queue and set their value in shouldQueueArray to -1.
  4. For each value in the queue, queue all of the blocks next to it or directly above that should be queued (water blocks that haven't yet been queued or checked), and set their values in the shouldQueueArray to -1
  5. Print the amount of 0's in the shouldQueueArray, which are the blocks that won't be drained and aren't part of the hill

With this approach every block will only be checked once, instead of the below method in which layers had to be ran through multiple times in the simulation. This method takes only about 2 seconds to run, which is once again significantly slower than the code below. This is likely the most optimized this solution is going to get, and I'm honestly a bit surprised this code has gotten to running so fast.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import math;
import random;

# Full data omitted so this code isn't 7 pages long, but data is stored as an array of strings, each line being a different string
heights = ["208047978591944048925234813...832872760452687289609809031"];
maxHeight = 10;
queue = [];
shouldQueueArray = [];

def enqueue(x, y, z):
    array = [x, y, z];
    queue.append(array);
    shouldQueueArray[y * maxHeight * len(heights) + x * maxHeight + z] = -1;

def shouldQueue(x, y, z):
    if(x == -1 or x == len(heights) or y == -1 or y == len(heights) or z < 0 or z >= maxHeight):
        return False;
    return shouldQueueArray[y * maxHeight * len(heights) + x * maxHeight + z] == 0;

def splitArray(num):
    array = [];
    for char in num:
        array.insert(len(array), int(char));
    return array;

def getLength():
    i = 0
    for item in shouldQueueArray:
        if(item == 0):
            i += 1;
    return i;

y = 0;
for heights_ in heights:
    x = 0;
    heightsArray = splitArray(heights_);
    for height in heightsArray:
        for h in range(0, height + 1):
            shouldQueueArray.append(-1);
        for h in range(height + 1, maxHeight):
            shouldQueueArray.append(0);
        if((x == 0 or x == len(heights) - 1 or y == 0 or y == len(heights) - 1) and height + 1 != maxHeight):
            enqueue(x, y, height + 1);
        x += 1;
    y += 1;

while(len(queue) != 0):
    pos = queue[0];
    queue.pop(0);
    if(shouldQueue(pos[0] - 1, pos[1], pos[2])):
        enqueue(pos[0] - 1, pos[1], pos[2]);
    if(shouldQueue(pos[0] + 1, pos[1], pos[2])):
        enqueue(pos[0] + 1, pos[1], pos[2]);
    if(shouldQueue(pos[0], pos[1] - 1, pos[2])):
        enqueue(pos[0], pos[1] - 1, pos[2]);
    if(shouldQueue(pos[0], pos[1] + 1, pos[2])):
        enqueue(pos[0], pos[1] + 1, pos[2]);
    if(shouldQueue(pos[0], pos[1], pos[2] + 1)):
        enqueue(pos[0], pos[1], pos[2] + 1);

print(getLength());


Seeing this problem for the first time at the start of a family road trip, I had many hours and, sadly, only my phone to write some program for this problem. While I didn't do much planning the general approach I took was the following:

  1. Create a Water class that stores a x, y, and z position that represents a block of water
  2. Create an array of Water objects that fill up a 100x100x10 square at the positions that aren't taken up by the hill
  3. Run through the array layer by layer starting with the top, and remove any water blocks that either are on the edge or are touching an empty square (which can only happen if they have some connection off the screen)

With some sort of plan in mind and plenty of time, I started implementing the code, testing what I had wrote on a couple of small sample cases for the problem. I decided to represent the data as an array of integers, and wrote a helper method create an array of each integer's digits. Finally, after quite some time, I had (what I thought was) a fully functioning program to calculate the remaining water. At last it was time to copy over the quite large 100x100 question data and watch the program run. After typing many commas I finally pressed run and waited for the program to complete, until...the code ran out of execution time. Dang it.

After spending a while optimizing my code I realized how long that program was going to take to run regardless of my efforts and decided to wait until I could copy it into my own computer, with the joys of unlimited execution time. Before postponing my quest the rest of the ride, I realized that any line starting with a zero would be improperly interpreted because the data was stored as an integer, so I decided to instead store that data as an array of strings (which also majorly simplified a pretty unreliable digit gathering method). Finally after getting back to our hotel I was able to run my code without the constraint of efficiency, and after about a pretty slow 20 minutes of waiting received an answer that turned out to be correct. I've consolidated my fairly messy code the best I could below, but I found this program to be quite fun and satisfying to create. The following code will successfully print out the water remaining for any square matrix of test data when given enough time, and I hope you will find it as interesting to interpret as it was for me to write.


Only about a day after writing this solution for the first time did I notice what was making this program so massively inefficient. The method to return a value at each position would have to create a gigantic array each time it was run, which was causing a majority of the time inefficiency. I realized that I could instead assign a value for which represents the hill, water, and air instead of just one for the water and "emptiness" (which would represent both the hill and the air). Now i could just return the value in the position array instead of referencing the hill, which shortened the run time from 20 minutes to...around 10 seconds! The program can now run completely in brilliant's coding environment, which is a major improvement from before. I've replaced the code below to be the efficient version, but have left the inefficient method below if you want to see what caused such a major time waste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import math;
import random;

# Full data omitted so this code isn't 7 pages long, but data is stored as an array of strings, each line being a different string
heights = ["208047978591944048925234813...832872760452687289609809031"];
waterArray = [];
queue = [];
maxHeight = 10;

def enqueue(x, y, z):
    array = [x, y, z];
    queue.append(array);

def splitArray(num):
    array = [];
    for char in num:
        array.insert(len(array), int(char));
    return array;

class Water:
    def __init__(self, x, y, z):
        self.x = x;
        self.y = y;
        self.z = z;

# 0 is hill, 1 is water, -1 is nothing
def getValue(x, y, h):
    if(x == -1 or x == len(heights) or y == -1 or y == len(heights)):
        return -1;
    if(isinstance(waterArray[y * maxHeight * len(heights) + x * maxHeight + h], Water)):
        return 1;
    return waterArray[y * maxHeight * len(heights) + x * maxHeight + h];

def removeValue(x, y, z):
    waterArray[y * maxHeight * len(heights) + x * maxHeight + z] = -1;

def getLength():
    i = 0
    for item in waterArray:
        if(isinstance(item, Water)):
            i += 1;
    return i;

y = 0;
for heights_ in heights:
    x = 0;
    heightsArray = splitArray(heights_);
    for height in heightsArray:
        for h in range(0, height + 1):
            waterArray.append(0);
        for h in range(height + 1, maxHeight):
            waterArray.append(Water(x, y, h));
        enqueue(x, y, height + 1);
        x += 1;
    y += 1;

height = maxHeight - 1;
while(height >= 0):
    updated = True;
    while(updated):
        updated = False;
        for y in range(0, len(heights)):
            for x in range(0, len(heights)):
                if(getValue(x, y, height) == 1 and (getValue(x + 1, y, height) == -1 or getValue(x - 1, y, height) == -1 or getValue(x, y + 1, height) == -1 or getValue(x, y - 1, height) == -1)):
                    removeValue(x, y, height);
                    updated = True;
    height -= 1;

print(getLength());

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# CRAZY Inefficient! 
# 0 is water, 1 is hill, 2 is nothing
def getValue(x, y, h):
    if(x == -1 or x == len(heights) or y == -1 or y == len(heights)):
        return 2;
    if(waterArray[y * 10 * len(heights) + x * 10 + h] != 0):
        return 0;
    list = heights[y];
    array = splitArray(list);
    if(array[x] >= h):
        return 1;
    return 2;

Hey, great job on the hard work. I hope you had fun!

So currently, your program depends on the maximum height of the hills. But something tells me that it might be possible to solve this in a way that is independent of this height. For example, let us say, we had the exact same numbers but multiplied every hills height by 10, in which case it intuitively looks like there should be a simpler way to solve this, rather than doing a layer by layer simulation.

I am guessing there could be breadth first search style approach where you could start filling in from the outerwalls where the water level is the lowest.

Agnishom Chattopadhyay - 2 years, 11 months ago

Log in to reply

Thanks! Because each value was being represented by a single digit I just limited the heights to 10, but for every new layer of height added the program does gets significantly slower. Now that you’ve mentioned breadth first searching I think I’ll try writing a much more efficient program with a queueing approach, where water connected to the bottommost water blocks for each height is removed. I’ll post that under my previous code once I’ve made some progress, thanks for the suggestion!

Calvin Osborne - 2 years, 11 months ago

Log in to reply

That'd be interesting. I look forward to seeing your idea in action.

Agnishom Chattopadhyay - 2 years, 11 months ago

0 pending reports

×

Problem Loading...

Note Loading...

Set Loading...