-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(python-algos): Add langtons ant
- Loading branch information
Showing
5 changed files
with
357 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
""" | ||
Langton's ant | ||
@ https://en.wikipedia.org/wiki/Langton%27s_ant | ||
@ https://upload.wikimedia.org/wikipedia/commons/0/09/LangtonsAntAnimated.gif | ||
""" | ||
|
||
from functools import partial | ||
|
||
from matplotlib import pyplot as plt | ||
from matplotlib.animation import FuncAnimation | ||
|
||
WIDTH = 80 | ||
HEIGHT = 80 | ||
|
||
|
||
class LangtonsAnt: | ||
""" | ||
Represents the main LangonsAnt algorithm. | ||
>>> la = LangtonsAnt(2, 2) | ||
>>> la.board | ||
[[True, True], [True, True]] | ||
>>> la.ant_position | ||
(1, 1) | ||
""" | ||
|
||
def __init__(self, width: int, height: int) -> None: | ||
# Each square is either True or False where True is white and False is black | ||
self.board = [[True] * width for _ in range(height)] | ||
self.ant_position: tuple[int, int] = (width // 2, height // 2) | ||
|
||
# Initially pointing left (similar to the wikipedia image) | ||
# (0 = 0° | 1 = 90° | 2 = 180 ° | 3 = 270°) | ||
self.ant_direction: int = 3 | ||
|
||
def move_ant(self, axes: plt.Axes | None, display: bool, _frame: int) -> None: | ||
""" | ||
Performs three tasks: | ||
1. The ant turns either clockwise or anti-clockwise according to the colour | ||
of the square that it is currently on. If the square is white, the ant | ||
turns clockwise, and if the square is black the ant turns anti-clockwise | ||
2. The ant moves one square in the direction that it is currently facing | ||
3. The square the ant was previously on is inverted (White -> Black and | ||
Black -> White) | ||
If display is True, the board will also be displayed on the axes | ||
>>> la = LangtonsAnt(2, 2) | ||
>>> la.move_ant(None, True, 0) | ||
>>> la.board | ||
[[True, True], [True, False]] | ||
>>> la.move_ant(None, True, 0) | ||
>>> la.board | ||
[[True, False], [True, False]] | ||
""" | ||
directions = { | ||
0: (-1, 0), # 0° | ||
1: (0, 1), # 90° | ||
2: (1, 0), # 180° | ||
3: (0, -1), # 270° | ||
} | ||
x, y = self.ant_position | ||
|
||
# Turn clockwise or anti-clockwise according to colour of square | ||
if self.board[x][y] is True: | ||
# The square is white so turn 90° clockwise | ||
self.ant_direction = (self.ant_direction + 1) % 4 | ||
else: | ||
# The square is black so turn 90° anti-clockwise | ||
self.ant_direction = (self.ant_direction - 1) % 4 | ||
|
||
# Move ant | ||
move_x, move_y = directions[self.ant_direction] | ||
self.ant_position = (x + move_x, y + move_y) | ||
|
||
# Flip colour of square | ||
self.board[x][y] = not self.board[x][y] | ||
|
||
if display and axes: | ||
# Display the board on the axes | ||
axes.get_xaxis().set_ticks([]) | ||
axes.get_yaxis().set_ticks([]) | ||
axes.imshow(self.board, cmap="gray", interpolation="nearest") | ||
|
||
def display(self, frames: int = 100_000) -> None: | ||
""" | ||
Displays the board without delay in a matplotlib plot | ||
to visually understand and track the ant. | ||
>>> _ = LangtonsAnt(WIDTH, HEIGHT) | ||
""" | ||
fig, ax = plt.subplots() | ||
# Assign animation to a variable to prevent it from getting garbage collected | ||
self.animation = FuncAnimation( | ||
fig, partial(self.move_ant, ax, True), frames=frames, interval=1 | ||
) | ||
plt.show() | ||
|
||
|
||
if __name__ == "__main__": | ||
import doctest | ||
|
||
doctest.testmod() | ||
|
||
LangtonsAnt(WIDTH, HEIGHT).display() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
""" | ||
A number container system that uses binary search to delete and insert values into | ||
arrays with O(log n) write times and O(1) read times. | ||
This container system holds integers at indexes. | ||
Further explained in this leetcode problem | ||
> https://leetcode.com/problems/minimum-cost-tree-from-leaf-values | ||
""" | ||
|
||
|
||
class NumberContainer: | ||
def __init__(self) -> None: | ||
# numbermap keys are the number and its values are lists of indexes sorted | ||
# in ascending order | ||
self.numbermap: dict[int, list[int]] = {} | ||
# indexmap keys are an index and it's values are the number at that index | ||
self.indexmap: dict[int, int] = {} | ||
|
||
def binary_search_delete(self, array: list | str | range, item: int) -> list[int]: | ||
""" | ||
Removes the item from the sorted array and returns | ||
the new array. | ||
>>> NumberContainer().binary_search_delete([1,2,3], 2) | ||
[1, 3] | ||
>>> NumberContainer().binary_search_delete([0, 0, 0], 0) | ||
[0, 0] | ||
>>> NumberContainer().binary_search_delete([-1, -1, -1], -1) | ||
[-1, -1] | ||
>>> NumberContainer().binary_search_delete([-1, 0], 0) | ||
[-1] | ||
>>> NumberContainer().binary_search_delete([-1, 0], -1) | ||
[0] | ||
>>> NumberContainer().binary_search_delete(range(7), 3) | ||
[0, 1, 2, 4, 5, 6] | ||
>>> NumberContainer().binary_search_delete([1.1, 2.2, 3.3], 2.2) | ||
[1.1, 3.3] | ||
>>> NumberContainer().binary_search_delete("abcde", "c") | ||
['a', 'b', 'd', 'e'] | ||
>>> NumberContainer().binary_search_delete([0, -1, 2, 4], 0) | ||
Traceback (most recent call last): | ||
... | ||
ValueError: Either the item is not in the array or the array was unsorted | ||
>>> NumberContainer().binary_search_delete([2, 0, 4, -1, 11], -1) | ||
Traceback (most recent call last): | ||
... | ||
ValueError: Either the item is not in the array or the array was unsorted | ||
>>> NumberContainer().binary_search_delete(125, 1) | ||
Traceback (most recent call last): | ||
... | ||
TypeError: binary_search_delete() only accepts either a list, range or str | ||
""" | ||
if isinstance(array, (range, str)): | ||
array = list(array) | ||
elif not isinstance(array, list): | ||
raise TypeError( | ||
"binary_search_delete() only accepts either a list, range or str" | ||
) | ||
|
||
low = 0 | ||
high = len(array) - 1 | ||
|
||
while low <= high: | ||
mid = (low + high) // 2 | ||
if array[mid] == item: | ||
array.pop(mid) | ||
return array | ||
elif array[mid] < item: | ||
low = mid + 1 | ||
else: | ||
high = mid - 1 | ||
raise ValueError( | ||
"Either the item is not in the array or the array was unsorted" | ||
) | ||
|
||
def binary_search_insert(self, array: list | str | range, index: int) -> list[int]: | ||
""" | ||
Inserts the index into the sorted array | ||
at the correct position. | ||
>>> NumberContainer().binary_search_insert([1,2,3], 2) | ||
[1, 2, 2, 3] | ||
>>> NumberContainer().binary_search_insert([0,1,3], 2) | ||
[0, 1, 2, 3] | ||
>>> NumberContainer().binary_search_insert([-5, -3, 0, 0, 11, 103], 51) | ||
[-5, -3, 0, 0, 11, 51, 103] | ||
>>> NumberContainer().binary_search_insert([-5, -3, 0, 0, 11, 100, 103], 101) | ||
[-5, -3, 0, 0, 11, 100, 101, 103] | ||
>>> NumberContainer().binary_search_insert(range(10), 4) | ||
[0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9] | ||
>>> NumberContainer().binary_search_insert("abd", "c") | ||
['a', 'b', 'c', 'd'] | ||
>>> NumberContainer().binary_search_insert(131, 23) | ||
Traceback (most recent call last): | ||
... | ||
TypeError: binary_search_insert() only accepts either a list, range or str | ||
""" | ||
if isinstance(array, (range, str)): | ||
array = list(array) | ||
elif not isinstance(array, list): | ||
raise TypeError( | ||
"binary_search_insert() only accepts either a list, range or str" | ||
) | ||
|
||
low = 0 | ||
high = len(array) - 1 | ||
|
||
while low <= high: | ||
mid = (low + high) // 2 | ||
if array[mid] == index: | ||
# If the item already exists in the array, | ||
# insert it after the existing item | ||
array.insert(mid + 1, index) | ||
return array | ||
elif array[mid] < index: | ||
low = mid + 1 | ||
else: | ||
high = mid - 1 | ||
|
||
# If the item doesn't exist in the array, insert it at the appropriate position | ||
array.insert(low, index) | ||
return array | ||
|
||
def change(self, index: int, number: int) -> None: | ||
""" | ||
Changes (sets) the index as number | ||
>>> cont = NumberContainer() | ||
>>> cont.change(0, 10) | ||
>>> cont.change(0, 20) | ||
>>> cont.change(-13, 20) | ||
>>> cont.change(-100030, 20032903290) | ||
""" | ||
# Remove previous index | ||
if index in self.indexmap: | ||
n = self.indexmap[index] | ||
if len(self.numbermap[n]) == 1: | ||
del self.numbermap[n] | ||
else: | ||
self.numbermap[n] = self.binary_search_delete(self.numbermap[n], index) | ||
|
||
# Set new index | ||
self.indexmap[index] = number | ||
|
||
# Number not seen before or empty so insert number value | ||
if number not in self.numbermap: | ||
self.numbermap[number] = [index] | ||
|
||
# Here we need to perform a binary search insertion in order to insert | ||
# The item in the correct place | ||
else: | ||
self.numbermap[number] = self.binary_search_insert( | ||
self.numbermap[number], index | ||
) | ||
|
||
def find(self, number: int) -> int: | ||
""" | ||
Returns the smallest index where the number is. | ||
>>> cont = NumberContainer() | ||
>>> cont.find(10) | ||
-1 | ||
>>> cont.change(0, 10) | ||
>>> cont.find(10) | ||
0 | ||
>>> cont.change(0, 20) | ||
>>> cont.find(10) | ||
-1 | ||
>>> cont.find(20) | ||
0 | ||
""" | ||
# Simply return the 0th index (smallest) of the indexes found (or -1) | ||
return self.numbermap.get(number, [-1])[0] | ||
|
||
|
||
if __name__ == "__main__": | ||
import doctest | ||
|
||
doctest.testmod() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.