diff --git a/homeworks/12_Ivaylo_Genchev/01_check_circles.py b/homeworks/13_Ivaylo_Genchev/01_check_circles.py similarity index 100% rename from homeworks/12_Ivaylo_Genchev/01_check_circles.py rename to homeworks/13_Ivaylo_Genchev/01_check_circles.py diff --git a/homeworks/13_Ivaylo_Genchev/02_grayscale_matrix_areas.py b/homeworks/13_Ivaylo_Genchev/02_grayscale_matrix_areas.py new file mode 100644 index 0000000..210f631 --- /dev/null +++ b/homeworks/13_Ivaylo_Genchev/02_grayscale_matrix_areas.py @@ -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() diff --git a/homeworks/13_Ivaylo_Genchev/02_reach_ladder_position.py b/homeworks/13_Ivaylo_Genchev/02_reach_ladder_position.py new file mode 100644 index 0000000..1e3a4d6 --- /dev/null +++ b/homeworks/13_Ivaylo_Genchev/02_reach_ladder_position.py @@ -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 diff --git a/homeworks/13_Ivaylo_Genchev/02_replace_collection_items.py b/homeworks/13_Ivaylo_Genchev/02_replace_collection_items.py new file mode 100644 index 0000000..58d2b82 --- /dev/null +++ b/homeworks/13_Ivaylo_Genchev/02_replace_collection_items.py @@ -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')]