Skip to content

Commit

Permalink
feat(python-algos): Add langtons ant
Browse files Browse the repository at this point in the history
  • Loading branch information
CaedenPH committed Dec 3, 2023
1 parent 7f68f87 commit 844a548
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 108 deletions.
106 changes: 106 additions & 0 deletions Algorithms/Python/langtons_ant.py
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()
180 changes: 180 additions & 0 deletions Algorithms/Python/number_container_system.py
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()
4 changes: 3 additions & 1 deletion Algorithms/Python/sudoku.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from random import choice, randint


class SudokuUtils:
"""
Represents the soduku utils that will be required
Expand Down Expand Up @@ -241,4 +242,5 @@ def print_board(self, raw: bool = False) -> None:

import timeit

print(timeit.timeit("SudokuGenerator().generate_puzzle('medium')", number=1000, globals=globals()))
cmd = "SudokuGenerator().generate_puzzle('medium')"
print(timeit.timeit(cmd, number=1000, globals=globals()))
Loading

0 comments on commit 844a548

Please sign in to comment.