Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions homeworks/13_Ivaylo_Genchev/02_grayscale_matrix_areas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from random import randint # generate random values in a matrix
from dataclasses import dataclass # cell class

"""
Задача 1:
Графично изображение е представено с матрица от m реда и n колони. Клетките на
матрицата, във всяка от които е записана целочислена стойност от 0 до 255, съответстват на пикселите
в графичното изображение (формат grayscale).
Всяка клетка в матрицата има до 8 съседа — до 4 по диагонал, до два, разположени хоризонтално,
и до два – вертикално.
Област в изображението е непрекъсната последователност от съседни клетки с ненулеви стойности.
Черните елементи, представени със стойност 0, се считат за контури на областите. Така, една област се
определя от граница от нулеви елементи и границите на матрицата.
Дефинирайте функция, `avg_brightness` която получава като аргумент матрица от посочения вид и
извежда на стандартния изход средната яркост на всяка от областите, сортирани в низходящ
ред според яркостта. Средна яркост на дадената област се изчислява като средно-аритметично на
стойностите на всички клетки (пиксели), образуващи областта. За всяка област изведете координатите
на една произволна клетка от нея и средната яркост на областта.
"""


@dataclass
class Cell:
row: int
column: int
value: int
is_accessed: bool = False # prevent getting the same area twice

def __int__(self):
return self.value


class Matrix:
def __init__(self, rows: int, columns: int, contour_chance: int):
"""Generate a random grayscale matrix rowsXcolumns where figurations have a contour_chance to appear over any brightness."""
if rows < 1:
raise ValueError(
f"Invalid value {rows}. 'rows' has to be a positive integer."
)

if columns < 1:
raise ValueError(
f"Invalid value {columns}. 'columns' has to be a positive integer."
)

if contour_chance < 1 or contour_chance > 100:
raise ValueError(
f"Invalid value {contour_chance}. 'contour_chance' has to be a positive integer."
)

self.grayscale_matrix: list[list[Cell]] = [
[
Cell(row=m, column=n, value=0)
if randint(1, 100) < contour_chance
else Cell(row=m, column=n, value=randint(0, 255))
for n in range(0, columns)
]
for m in range(0, rows)
]
self.rows = rows
self.columns = columns

def surrounding_cells(self, cell: Cell) -> list[Cell]:
"""Get a list of cells surrounding a cell."""
surrounding: list[Cell] = []

if cell.row != 0: # upper
surrounding.append(self.grayscale_matrix[cell.row - 1][cell.column])
if cell.column != 0: # upper left
surrounding.append(self.grayscale_matrix[cell.row - 1][cell.column - 1])
if cell.column < self.columns - 1: # upper right
surrounding.append(self.grayscale_matrix[cell.row - 1][cell.column + 1])

if cell.row < self.rows - 1: # bottom
surrounding.append(self.grayscale_matrix[cell.row + 1][cell.column])
if cell.column != 0: # bottom left
surrounding.append(self.grayscale_matrix[cell.row + 1][cell.column - 1])
if cell.column < self.columns - 1: # bottom right
surrounding.append(self.grayscale_matrix[cell.row + 1][cell.column + 1])

if cell.column != 0: # left
surrounding.append(self.grayscale_matrix[cell.row][cell.column - 1])

if cell.column < self.columns - 1: # right
surrounding.append(self.grayscale_matrix[cell.row][cell.column + 1])

return surrounding

def get_area(self, cell: Cell, area: list[Cell], total_brightness: int = 0) -> int:
"""
Get a list of all cells surrounded by contours (cells with no brightness).
List is written in 'area'.
Function returns total brightness of all cells in the area.
"""

if cell.is_accessed == False and cell.value != 0:
# append cell if it hasn't been accessed (avoid repeats) and isn't a contour
area.append(cell)
cell.is_accessed = True
total_brightness += cell.value

for surrounding in self.surrounding_cells(cell):
total_brightness = self.get_area(surrounding, area, total_brightness)

return total_brightness

def avg_brightness(self):
"""Calculate average brightness of all areas."""

areas: list[tuple[tuple[Cell], int]] = []

for row in self.grayscale_matrix:
for cell in row:
if cell.is_accessed == False and cell.value != 0: # redundant
area: list[Cell] = []
total_brightness: int = self.get_area(cell, area)

# append area tuple and avrg brightness
areas.append((tuple(area), total_brightness / len(area)))

# sort new list based on average brightness in descending order
areas.sort(reverse=True, key=lambda x: x[1])

# print info
for area_info in areas:
print("-----------------------------------------------------------")
print(f"Cells in area: {len(area_info[0])}")
random_cell: Cell = area_info[0][randint(0, len(area_info[0]) - 1)]
print(
f"Random cell ({random_cell.value}): ({random_cell.row},{random_cell.column})"
)
print(f"Average brightness: {round(area_info[1], 2)}")

def __str__(self) -> str:
matrix_str: str = ""
most_number_digits: int = 3

for row in self.grayscale_matrix:
for cell in row:

matrix_str += f" {cell.value}{(most_number_digits - len(str(cell.value)) + 1) * ' '}"
matrix_str += "\n\n\n"

return matrix_str


if __name__ == "__main__":
matrix: Matrix = Matrix(rows=8, columns=8, contour_chance=70)
print(matrix)
matrix.avg_brightness()
93 changes: 93 additions & 0 deletions homeworks/13_Ivaylo_Genchev/02_reach_ladder_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from inspect import get_annotations # type check function arguments

# check python version (differt ways to type check annotations)
from sys import version_info

"""
Дадена е стълба с N >= 2 стъпала. Стоим в началото на стълбата и можем да качим 1 или 2 стъпала наведнъж.
Да се напише функция, `num_ways` която по подадено N връща броя на начини по които можем да изкачим стълбата - т.е. да стъпим на N-тото стъпало.
Пример: при N = 2 функцията връща 2 - можем да минем по всяко стъпало (начин 1) или да се качим директно на второто (начин 2)
при N=3 функцията връща 3 - можем да качим трите стъпала едно по едно (начин 1), да изкачим 1 стъпало и осналите наведнъж (начин 2) или да изкачим първите 2 стъпала наведнъж и после третото (начин 3)
"""


def num_ways(steps: int) -> int:
"""
Function to calculate the amount of different ways a ladder can be climbed to the nth step.
Acts as an error handler.
"""

# N cannot be less than 2 (according to task requirement)
if steps < 2:
raise ValueError(
f"Invalid input '{steps}'. Amount of steps to climb must be greater than 1."
)

if version_info[1] < 10: # get minor python version (for type checking)
# PEP compliant way to access annotations before 3.10
if type(steps) != num_ways.__annotations__["steps"]:
raise TypeError(f"Argument 'steps' cannot be of type {type(steps)}.")
else: # 3.10+
# PEP compliant way to access annotatinos (3.10+)
if type(steps) != get_annotations(num_ways)["steps"]:
raise TypeError(f"Argument 'steps' cannot be of type {type(steps)}.")

def calculate_ways(remaining_steps: int, ways_amount: int = 0) -> int:
"""Recursively calculate the amount of ways N steps can be climbed."""

# exit condition
if remaining_steps < 0:
# don't change ways_amount due invalid movement (1 step was remaining but 2 were climbed)
return ways_amount
elif remaining_steps == 0:
return ways_amount + 1

# try to climb steps 1 and 2 at a time
ways_amount = calculate_ways(remaining_steps - 1, ways_amount)
ways_amount = calculate_ways(remaining_steps - 2, ways_amount)

return ways_amount

return calculate_ways(steps)


def fibonacci(n: int) -> int:
"""
Calculate the next fibonacci number.
Could be faster if a precomputed dict of the fibonacci sequence is used.
"""

# type check
if version_info[1] < 10: # get minor python version (for type checking)
if type(n) != fibonacci.__annotations__["n"]:
# PEP compliant way to access annotations before 3.10
raise TypeError(f"Argument 'n' cannot be of type {type(n)}.")
else: # 3.10+
if type(n) != get_annotations(fibonacci)["n"]:
# PEP compliant way to access annotations (3.10+)
raise TypeError(f"Argument 'n' cannot be of type {type(n)}.")

first: int = 0
second: int = 1

if n < 1:
raise ValueError(f"Nth fibonacci number cannot be {n}. Has to be > 1!")
elif n == 1:
return first
elif n == 2:
return second
else:
for i in range(1, n):
result: int = first + second
first = second
second = result

return second


if __name__ == "__main__":
step: int = 5
print(f"Standard recursion sollution: {num_ways(step)}")
print(
f"Using fibonacci sequence (better performance): {fibonacci(step+1)}"
) # step+1 because first fibonacci number is 0, essentially callibrating the function
55 changes: 55 additions & 0 deletions homeworks/13_Ivaylo_Genchev/02_replace_collection_items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
Задача 3:
Даден е Python списък от елементи - низове, числа, списъци (вкл. като този), наредени н-торки. Да се напише функция `replace` с 3 аргумента - `list`, `find`, `replace`, където list e писък от типа по-горе, a find и replace са низове. Функцията връща нова версия на `list` в която всяко срещане на `find` e заменено с `replace`.
Пример:
list = [ 'a', 1, [ ['a', 'b'], 1], ([1, 3, 'a'], 'b')]
res = replace(list, 'a', 'c')
print(res) # => [ 'c', 1, [ ['c', 'b'], 1], ([1, 3, 'c'], 'b')]
"""


def replace(
collection: list[str | int | list | tuple] | tuple[str | int | list | tuple],
find: str | int,
new: str | int,
) -> list[str | int | list | tuple] | tuple[str | int | list | tuple]:
"""Replace all occurances of 'find' in 'collection' with 'new'."""

# copy list (preserve original as immutable)
if isinstance(collection, list):
new_collection = collection.copy()
elif isinstance(collection, tuple):
new_collection = list(collection) # make tuple mutable
else:
raise ValueError(f"unsupported type: {type(collection)}")

# iterate over all elements inside the list
for i, element in enumerate(new_collection):
# replace if element value is equal to find
if element == find:
new_collection[i] = new
# if element is iterable, iterate over it the same way
# since strings are iterable, they could cause infinite recursion
elif hasattr(element, "__iter__") and not isinstance(element, str):
if isinstance(element, tuple) or isinstance(collection, tuple):
# convert element to tuple (if it has been converted to list)
new_collection[i] = tuple(replace(element, find, new))
else:
new_collection[i] = replace(element, find, new)

return new_collection


if __name__ == "__main__":
collection: list[str | int | list | tuple] = [
"a",
1,
[["a", "b"], 1],
([1, 3, "a"], "b"),
]
find: str = "a"
new: str = "c"

res = replace(collection, find, new)
print(f"Unchanged collection: {collection}")
print(f"Having replaced '{find}' with '{new}': {res}") # => [ 'c', 1, [ ['c', 'b'], 1], ([1, 3, 'c'], 'b')]