Skip to content

Commit e16dfa8

Browse files
committed
added digiIslands with tests
1 parent 306d0d9 commit e16dfa8

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

ml/digiIslands/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Digital islands
2+
3+
> Using a boolean 2D matrix to represent islands. Any connected "\*"s form an island. For example:
4+
5+
```python
6+
matrix = [
7+
[*, *, _, _, _, *, _],
8+
[_, *, *, _, _, _, _],
9+
[_, _, _, _, *, _, _],
10+
[_, _, *, _, _, _, *],
11+
[*, _, *, *, _, _, *],
12+
[_, *, *, _, _, _, _],
13+
[_, _, _, _, _, _, _],
14+
[_, _, *, _, *, *, *],
15+
]
16+
```
17+
> Above has 6 islands, and the largest island size is 7.
18+
19+
> See https://repl.it/@jason_zhuyx/digiIslands
20+
21+
The `DigiIslands` class scans an input matrix and builds a list of island slices, with visited history info, to achieve `O(2*n)` in space and `O(n)` for runtime execution, where "`n`" is number of items of the input matrix.
22+
23+
```python
24+
from ml.digiIslands.di import DigiIslands
25+
from tests.constant import DATA
26+
27+
import ml.common.extension as utils
28+
29+
30+
def main():
31+
"""
32+
The main routine.
33+
"""
34+
# import sys
35+
# print('command args', sys.argv)
36+
# return sys.argv
37+
data = DATA[3]
38+
print("- input matrix: {}".format(utils.str_matrix(data)))
39+
grid = DigiIslands(data)
40+
grid.print_islands()
41+
42+
43+
if __name__ == "__main__":
44+
import os
45+
os.system('python3 -m unittest -v tests/test_di.py')
46+
# run_all()
47+
main()
48+
```

ml/digiIslands/__init__.py

Whitespace-only changes.

ml/digiIslands/di.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
class DigiIslands
3+
"""
4+
import ml.utils.extension as utils
5+
6+
COORD_X = 0 # index of coordinate x
7+
COORD_Y = 1 # index of coordinate y
8+
LANDTAG = 1 # value to indicate solid land
9+
10+
11+
class DigiIslands:
12+
"""
13+
DigiIslands uses input from a 2D matrix to build representation of digital islands.
14+
"""
15+
16+
def __init__(self, matrix: list):
17+
"""Construct a DigiIslands object."""
18+
if not isinstance(matrix, list) or \
19+
len(matrix) < 1 or \
20+
not isinstance(matrix[0], list):
21+
raise TypeError("matrix must be a non-empty list of list.")
22+
self.data = matrix
23+
self.rows = len(matrix)
24+
self.cols = len(matrix[0]) if self.rows > 0 else 0
25+
self.num = 0 # number of islands
26+
self._b = [] # boundary of 2 coordinates [x, y] as top-left and bottom-right corners, slice of slices
27+
self._m = [] # group of coords [x, y] to represent land digits, slice of slices
28+
self._v = [
29+
[False for _ in range(self.cols)] for _ in range(self.rows)
30+
] # visited coords
31+
self.bigO = 0 # big O(n) of calculation
32+
self.bigO_exec = 0 # big O(n) of statements
33+
self.biggest_size = 0
34+
self.biggest_island_index = None
35+
self.build_islands()
36+
37+
def __str__(self):
38+
"""String method"""
39+
return utils.str_matrix(self._m)
40+
41+
def build_islands(self):
42+
"""
43+
Build islands from matrix data.
44+
"""
45+
for x in range(self.rows):
46+
for y in range(self.cols):
47+
if self._v[x][y]:
48+
continue
49+
self.bigO += 1
50+
self.bigO_exec += 1
51+
if self.data[x][y] == LANDTAG:
52+
self.join_islands(x, y)
53+
self._v[x][y] = True
54+
# self.print_islands()
55+
pass
56+
57+
def check_surroundings(self, x, y, g):
58+
"""
59+
Check surrounding coords of (x, y) to join group g.
60+
"""
61+
for dx in [-1, 0, 1]:
62+
for dy in [-1, 0, 1]:
63+
self.bigO_exec += 1
64+
a, b = x + dx, y + dy
65+
if (a == x and b == y) or \
66+
a < 0 or a >= self.rows or \
67+
b < 0 or b >= self.cols or \
68+
self._v[a][b]:
69+
# print("- skip [{}, {}]".format(a, b))
70+
continue
71+
# print("- check surrounding ({}, {}) of ({}, {}) for group {}".format(a, b, x, y, g))
72+
self.bigO += 1
73+
if self.data[a][b] == LANDTAG:
74+
self.join_islands(a, b, g)
75+
self._v[a][b] = True
76+
pass
77+
78+
def join_islands(self, x, y, g=None):
79+
"""
80+
Join a coordinate (x, y) to a group.
81+
"""
82+
# print("- join ({}, {}) to group {}".format(x, y, g))
83+
# check boundary
84+
if g is None:
85+
g, self.num = self.num, self.num + 1
86+
self._m.append([])
87+
88+
self._m[g].append([x, y]) # add to the group
89+
self._v[x][y] = True
90+
91+
# check biggest island
92+
if len(self._m[g]) > self.biggest_size:
93+
self.biggest_size = len(self._m[g])
94+
self.biggest_island_index = g
95+
96+
# create or update bounary
97+
self.set_boundary(x, y, g)
98+
99+
# check surrounding coords of (x, y)
100+
self.check_surroundings(x, y, g)
101+
pass
102+
103+
def print_islands(self):
104+
print("- built islands ({}, biggest size {}): {}".format(
105+
self.num, self.biggest_size, self))
106+
107+
def set_boundary(self, x, y, g):
108+
"""
109+
Create or update boundary of a group at coord (x, y).
110+
"""
111+
if g >= len(self._b):
112+
# print("- create new boundary [{}]({}, {})".format(g, x, y))
113+
self._b.append([[x, y], [x, y]])
114+
return
115+
116+
# print("- update boundary {} at ({}, {}) - len(g)={}".format(g, x, y, len(self._b)))
117+
# get top-left and bottom-right
118+
tl, br = self._b[g][0], self._b[g][1]
119+
tl[COORD_X] = x if x < tl[COORD_X] else tl[COORD_X]
120+
tl[COORD_Y] = y if y < tl[COORD_Y] else tl[COORD_Y]
121+
br[COORD_X] = x if x > br[COORD_X] else br[COORD_X]
122+
br[COORD_Y] = y if y > br[COORD_Y] else br[COORD_Y]
123+
# self._b[g] = [tl, br]
124+
pass
125+
126+
def str(self):
127+
return "digiIslands [{}x{}] ({}, biggest size {}): {}".format(
128+
self.rows, self.cols, self.num, self.biggest_size, self)

ml/utils/extension.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,14 @@ def pickle_to_str(obj, *rm_keys):
290290
json_obj = pickle_object(obj, *rm_keys)
291291

292292
return json.dumps(json_obj)
293+
294+
295+
def str_matrix(v):
296+
"""
297+
Get beautified string format of a matrix.
298+
"""
299+
s = "[\n"
300+
for rows in v:
301+
s += " " + str(rows) + ",\n"
302+
s += "]"
303+
return s

tests/test_di.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
digiIslands tests
3+
"""
4+
import unittest
5+
6+
from ml.digiIslands.di import DigiIslands
7+
from tests.test_di_data import TEST_DATA
8+
9+
10+
class TestDigiIslands(unittest.TestCase):
11+
"""
12+
DigiIslands Test
13+
"""
14+
def setUp(self):
15+
"""
16+
test setup
17+
"""
18+
self.tests = TEST_DATA
19+
pass
20+
21+
def test_build_islands(self):
22+
"""
23+
test build_islands function
24+
"""
25+
print("test DigiIslands")
26+
for idx, test in enumerate(self.tests):
27+
biggest_size = test["biggest"]
28+
num_of_islands = test["num"]
29+
matrix = test["data"]
30+
di = DigiIslands(matrix)
31+
print("Test {:02d} | O({} / {}) | {}".format(
32+
idx, di.bigO, di.bigO_exec, di.str()))
33+
self.assertEqual(di.biggest_size, biggest_size)
34+
self.assertEqual(di.num, num_of_islands)
35+
print("")
36+
37+
def test_exceptions(self):
38+
tests = [
39+
"",
40+
None,
41+
"this is a string",
42+
{"a": 1, "b": 2, "c": 3},
43+
[1, 2, 3, 4],
44+
]
45+
for test in tests:
46+
with self.assertRaises(TypeError) as context:
47+
DigiIslands(test)
48+
self.assertEqual(
49+
str(context.exception),
50+
'matrix must be a non-empty list of list.')
51+
52+
di = DigiIslands([[]])
53+
di.print_islands()

tests/test_di_data.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
constant data for 2D matrix islands.
3+
"""
4+
5+
_ = 0 # represent water
6+
x = 1 # represent solid land
7+
8+
DATA = [[
9+
[_, _, _, _, _, _, _, _],
10+
[_, _, _, _, _, _, _, _],
11+
[_, _, _, _, _, _, _, _],
12+
[_, _, _, _, _, _, _, _],
13+
[_, _, _, _, _, _, _, _],
14+
[_, _, _, _, _, _, _, _],
15+
[_, _, _, _, _, _, _, _],
16+
[_, _, _, _, _, _, _, _], # 0 (0, biggest size 0)
17+
], [
18+
[_, _, _, _, _, _, _, _],
19+
[x, x, x, _, _, x, x, _],
20+
[_, x, x, x, _, _, x, _],
21+
[_, _, _, x, x, _, _, _],
22+
[x, _, _, _, _, _, _, _],
23+
[x, _, _, _, x, x, _, _],
24+
[x, _, _, x, x, _, _, _],
25+
[_, _, _, _, _, _, _, _], # 1 (4, biggest size 8)
26+
], [
27+
[x, x, _, _, x, _, _, _],
28+
[_, x, x, _, _, _, _, _],
29+
[_, _, _, _, x, _, _, _],
30+
[_, _, _, _, _, _, _, x],
31+
[x, _, x, x, _, _, x, _],
32+
[_, x, x, _, _, _, _, _],
33+
[_, _, x, _, _, _, _, _],
34+
[_, _, x, _, x, x, x, x], # 2 (6, biggest size 7)
35+
], [
36+
[x, x, _, _, _, _, _, x],
37+
[_, x, x, _, _, x, _, _],
38+
[_, _, x, _, x, _, _, _],
39+
[_, _, _, _, _, _, x, _],
40+
[x, _, _, x, x, _, x, _],
41+
[_, x, x, _, _, _, _, _],
42+
[_, _, x, _, _, _, _, _],
43+
[x, _, _, _, x, x, x, x], # 3 (7, biggest size 6)
44+
], [
45+
[x, _, _, _, _, _, _, x],
46+
[_, _, _, _, _, _, _, _],
47+
[_, _, _, _, _, _, _, _],
48+
[_, _, _, _, _, _, _, _],
49+
[_, _, _, _, _, _, _, _],
50+
[x, _, _, _, _, _, _, x], # 4 (4, biggest size 1)
51+
], [
52+
[_, _, _, _, _, _, _, _, _],
53+
[_, _, _, _, _, _, _, _, _],
54+
[_, _, _, _, _, _, _, _, _],
55+
[_, _, _, _, x, _, _, _, _],
56+
[_, _, _, _, _, _, _, _, _],
57+
[_, _, _, _, _, _, _, _, _],
58+
[_, _, _, _, _, _, _, _, _], # 5 (1, biggest size 1)
59+
], [
60+
[x, x, x, x, x, x, x, x],
61+
[x, x, x, x, x, x, x, x],
62+
[x, x, x, x, x, x, x, x],
63+
[x, x, x, x, x, x, x, x],
64+
[x, x, x, x, x, x, x, x],
65+
[x, x, x, x, x, x, x, x],
66+
[x, x, x, x, x, x, x, x],
67+
[x, x, x, x, x, x, x, x], # 6 (1, biggest size 64)
68+
], [
69+
[x, _, _, _, _, _, _, _],
70+
[x, _, _, _, _, _, _, _],
71+
[x, _, _, _, _, _, _, _],
72+
[x, _, _, _, _, _, _, _],
73+
[x, _, _, _, _, _, _, _], # 7 (1, biggest size 5)
74+
], [
75+
[x, _, _, x, _, _, _, _],
76+
[_, _, x, _, _, x, _, _],
77+
[_, x, _, _, _, _, x, _],
78+
[x, _, _, _, _, _, _, x], # 3, biggest size 4
79+
], [
80+
[x, _, _, _, _, _, _, x],
81+
[_, x, _, _, _, x, x, x],
82+
[x, _, _, _, _, _, _, x], # 2, biggest size 5
83+
]]
84+
85+
TEST_DATA = [
86+
{"data": DATA[0], "num": 0, "biggest": 0},
87+
{"data": DATA[1], "num": 4, "biggest": 8},
88+
{"data": DATA[2], "num": 6, "biggest": 7},
89+
{"data": DATA[3], "num": 7, "biggest": 6},
90+
{"data": DATA[4], "num": 4, "biggest": 1},
91+
{"data": DATA[5], "num": 1, "biggest": 1},
92+
{"data": DATA[6], "num": 1, "biggest": 64},
93+
{"data": DATA[7], "num": 1, "biggest": 5},
94+
{"data": DATA[8], "num": 3, "biggest": 4},
95+
{"data": DATA[9], "num": 2, "biggest": 5},
96+
{
97+
"data": [[_]], "num": 0, "biggest": 0,
98+
}, {
99+
"data": [[x]], "num": 1, "biggest": 1,
100+
}, {
101+
"data": [[_, _, _, _]], "num": 0, "biggest": 0,
102+
}, {
103+
"data": [[x], [x], [x], [x], [x]], "num": 1, "biggest": 5,
104+
}, {
105+
"data": [[x, x, x, x, x]], "num": 1, "biggest": 5,
106+
}, {
107+
"data": [[_, x, x, _]], "num": 1, "biggest": 2,
108+
}, {
109+
"data": [[x, _, x]], "num": 2, "biggest": 1,
110+
}, {
111+
"data": [[]], "num": 0, "biggest": 0,
112+
},
113+
]

0 commit comments

Comments
 (0)