You are on page 1of 35

Міністерство освіти і науки України

Національний технічний університет України


“Київський політехнічний інститут імені Ігоря Сікорського”
ФІОТ
ІСТ

Лабораторна робота №2
по курсу “Теорія алгоритмів”
на тему “Методи розробки алгоритмів. Частина 1.”

Варіант 7

Виконали ст групи ІС-23:


Коваль Ярослав
Красовський Микола
Вершигора Олександр
Перевірив: ст. вик. Дорошенко К. С.

2023
Комп’ютерний практикум 3
Тема: Методи розробки алгоритмів. Частина 1.
Мета роботи: Порівняння алгоритмів розв’язку задачі, побудованих
різними методами.
-Звіт-
Завдання
1. Для свого варіанту зробити наступні дії:
1) Сформулювати постановку задачі
2) Обрати відповідні 2 алгоритми з теоретичної частини практикуму або
на свій розсуд
3) Накреслити блок-схеми, на яких виконано аналіз складності алгоритмів
4) Написати програмний код
5) Провести дослідження продуктивності роботи алгоритмів, зробити
результати дослідження у вигляді графіків та діаграм;
6) Зробити висновки про доцільність використання кожного з алгоритмів
для типових вхідних даних та про відповідність результатів
експериментального дослідження аналітичним оцінкам складності.
7) Скласти таблицю тестування.
8) Навести скріншоти роботи програми. для заданої задачі
9) Дати відповіді на контрольні питання
Варіанти завдань
В Києві є декілька цікавих місць, а саме:
1. Червоний університет
2. Андріївська церква
3. Михайлівський собор
4. Золоті ворота
5. Лядські ворота
6. Фунікулер
7. Київська політехніка
8. Фонтан на Хрещатику
9. Софія київська
10. Національна філармонія
11. Музей однієї вулиці
Необхідно створити графічне зображення сполучень між ними,
враховуючи наступні
Постановка задачі
Не всі студенти НТУУ «КПІ ім. Ігоря Сікорського» полюбляють гуляти
пішки, тому компанія хлопців і дівчат з університету вирішила прокласти
транспортні маршрути від свого навчального закладу до інших
вищеперерахованих місць за умови найменшої вартості такого проекту.
Допоможіть їм прокласти такі маршрути.
Вхідні дані
1. Список місць, які студент хоче відвідати.
2. Інформація про доступні види транспорту та їхню вартість на
маршрутах між гуртожитком та кожним з місць.
Вихідні елементи
Список найдешевших маршрутів від гуртожитку до вище перерахованих
місць.
Інтерфейс користувача
Було прийнято рішення додати простий графічний інтерфейс користувача:
Користувач має змогу обрати алгоритм, за допомогою якого буде
здійснюватися пошук найдешевших маршрутів, та кінцеву точку із
випадаючого списку. Натиснувши кнопку “Виконати”, на екран
виводяться ціна маршруту та контрольні точки, якими проходить цей
маршрут. Завдяки цьому користувач не здатний нашкодити програмі,
ввівши неправильну інформацію. Адже всі можливі варіанти, які може
обрати користувач, виведені на головному інтерфейсі. Тому навіть
недосвідчені юзери не заплутаються у програмі.
Математична модель
Основні величини:
graph Graph Oб’єкт графа
graph.nodes List[Node] Масив точок графу
graph.adj_list List[Vertex] Список суміжності
graph.edges List[Edge] Масив ребер графу
vertex Vertex Вершина графу
vertex.node Node До якої точки
належить граф
vertex.edges List[Edge] Масив ребер вершини
edge Edge Oб’єкт ребра
edge.start Node Початкова точка ребра
edge.end Node Кінцева точка ребра
edge.weight float Вага ребра
node Node Об’ект точки графу
node.data string Опис точки
node.index int індекс точки у графі
Розробка алгоритму (Блок-схема)

Алгоритм Беллмана Форда


Аналіз складності алгоритму
I. Алгоритм Дейкстри
Алгоритм Дейкстри має часову складність O(E log V), де V - кількість
вершин у графі, а E - кількість ребер у графі.
Алгоритм складається з V ітерацій, кожна з яких складається з таких
етапів:
1. Знаходження вершини з мінімальною відстанню: O(log V) за
допомогою пошуку в мінімальній купі, яка може мати до V
елементів.
2. Позначення знайденої вершини як відвіданої: O(1).
3. Оновлення відстаней до всіх сусідніх вершин, що ще не відвідали:
O(E) у гіршому випадку для кожної вершини.
Оскільки кожна з V вершин буде відвідана, а кожне ребро буде розглянуто
один раз для кожної вершини, загальна часова складність алгоритму
Дейкстри буде O((E + V) log V).
У найгіршому випадку, коли граф має багато ребер та мало вершин,
складність може бути близькою до O(E log E). Однак, у найкращому
випадку, коли граф має мало ребер, складність буде близькою до O(V log
V).
Отже, алгоритм Дейкстри ефективний для застосування у графах з
невеликою кількістю ребер або коли кількість ребер не залежить від
кількості вершин, наприклад, у розріджених графах. Але у графах з
великою кількістю ребер він може бути менш ефективним, і тоді більш
доцільним може бути використання алгоритмів, що базуються на пошуку в
ширину, таких як BFS, або алгоритмів, що використовують найкоротший
шлях до кожної вершини, таких як алгоритм Беллмана-Форда.
II. Алгоритм Беллмана-Форда
Алгоритм Беллмана-Форда має часову складність O(VE), де V - кількість
вершин у графі, а E - кількість ребер у графі.
Алгоритм складається з V ітерацій, кожна з яких складається з таких
етапів:
1. Проходження по всіх ребрах графу: O(E).
2. Оновлення відстаней до всіх сусідніх вершин, якщо це необхідно:
O(1).
Оскільки кожне ребро буде розглянуто один раз для кожної з V вершин,
загальна часова складність алгоритму Беллмана-Форда буде O(VE).
У найгіршому випадку, коли граф має багато ребер та мало вершин,
складність може бути близькою до O(E^2). Однак, у найкращому випадку,
коли граф має мало ребер, складність буде близькою до O(V).
Алгоритм Беллмана-Форда є ефективним для застосування у графах з
багатьма ребрами, але зазвичай менш ефективним у порівнянні з
алгоритмом Дейкстри у тих випадках, коли граф має мало ребер. Однак,
він може бути корисним у випадках, коли граф має від’ємні ваги ребер,
оскільки алгоритм Дейкстри не працює з від'ємними вагами.
Правильність алгоритму
I. Алгоритм Дейкстри
Алгоритм Дейкстри є коректним для пошуку найкоротших шляхів у
зваженому графі з невід'ємними ребрами.
Алгоритм Дейкстри працює наступним чином:
1. Встановлюємо відстані від початкової вершини до всіх інших
вершин графа рівними нескінченності, окрім початкової вершини,
для якої відстань дорівнює нулю.
2. Знаходимо вершину з найменшою відстанню до початкової
вершини. Ця вершина буде поточною вершиною.
3. Проводимо релаксацію (оновлення відстаней) для кожного сусідньої
вершини поточної вершини (v, w), де w - сусідня вершина, а v -
поточна вершина. Якщо відстань до w можна скоротити, тобто
відстань до v плюс вага ребра weight(v, w) менша за поточну
відстань до w, то оновлюємо відстань до w.
4. Повторюємо крок 2 та 3 до тих пір, поки всі вершини не будуть
оброблені.
Після закінчення алгоритму ми знаходимо найкоротший шлях до кожної
вершини.
Отже, алгоритм Дейкстри є коректним і забезпечує знаходження
найкоротших шляхів у зваженому графі з невід'ємними ребрами. Однак,
якщо у графі є від'ємні ребра, то цей алгоритм може не працювати
правильно.
II. Алгоритм Беллмана-Форда
Алгоритм Беллмана-Форда є коректним для пошуку найкоротших шляхів
в зваженому графі з від'ємними ребрами, якщо такі існують.
Алгоритм Беллмана-Форда працює наступним чином:
1. Встановлюємо відстані від початкової вершини до всіх інших
вершин графа рівними нескінченності, окрім початкової вершини,
для якої відстань дорівнює нулю.
2. Проводимо релаксацію (оновлення відстаней) для кожного ребра
графа (v, w) з вагою weight(v, w), де v та w - кінцеві вершини ребра.
Якщо відстань до w можна скоротити, тобто відстань до v плюс вага
ребра weight(v, w) менша за поточну відстань до w, то оновлюємо
відстань до w.
3. Повторюємо крок 2 n-1 разів, де n - кількість вершин в графі. Це
необхідно, оскільки найкоротший шлях в графі може проходити
через не більше, ніж n-1 ребер.
4. Перевіряємо наявність циклів в графі з від'ємною вагою ребер. Якщо
в процесі n-1 релаксацій з'являється можливість скоротити відстань
до якоїсь вершини, то у графі є цикл з від'ємною вагою ребер. В
такому випадку алгоритм неможливо застосувати, оскільки немає
найкоротшого шляху до деяких вершин.
Отже, алгоритм Беллмана-Форда є коректним і забезпечує знаходження
найкоротших шляхів в зваженому графі з від'ємними ребрами, якщо такі
існують.
Текст програми
import sys

import time

from functools import wraps

from typing import List, Union, Optional, Generic, TypeVar

from PyQt6 import QtCore, QtGui, QtWidgets

from PyQt6.QtGui import QIcon

from PyQt6.QtWidgets import QApplication

T = TypeVar('T')

def timeit(func):

@wraps(func)

def timeit_wrapper(*args, **kwargs):

start_time = time.perf_counter()

result = func(*args, **kwargs)

end_time = time.perf_counter()

total_time = end_time - start_time

print(f'Function {func.__name__}{args} {kwargs} Took {total_time:.4f}


seconds')

return result

return timeit_wrapper

class Node:

__slots__ = [

"data",

"index"
]

data: str

index: Optional[int]

def __init__(self, data: str):

self.data = data

self.index = None

def __repr__(self) -> str:

return f"{self.data}"

def __str__(self) -> str:

return f"{self.data}"

class Edge:

__slots__ = [

"start",

"end",

"weight"

start: Node

end: Node

weight: float

def __init__(self, start: Node, end: Node, weight: float):

self.start = start

self.end = end

self.weight = weight

class Vertex:

__slots__ = [

"node",

"edges"

]
node: Node

edges: List[Edge]

def __init__(self, node: Node, edges: Optional[List[Edge]] = None):

self.node = node

if edges is None:

self.edges = []

else:

self.edges = edges

def append(self, edge: Edge) -> None:

self.edges.append(edge)

class NodeDecorator:

__slots__ = [

"node",

"prov_dist",

"hops"

node: Node

prov_dist: float

hops: List[Node]

def __init__(self, node: Node):

self.node = node

self.prov_dist = float("inf")

self.hops = []

def append_hop(self, hop: Node) -> None:

self.hops.append(hop)

def __eq__(self, other: "NodeDecorator") -> bool:

return self.prov_dist == other.prov_dist


def __lt__(self, other: "NodeDecorator") -> bool:

return self.prov_dist < other.prov_dist

def __gt__(self, other: "NodeDecorator") -> bool:

return self.prov_dist > other.prov_dist

def __repr__(self) -> str:

return f"{self.node} - {self.prov_dist}"

def __str__(self) -> str:

return f"{self.node} - {self.prov_dist}"

class MinHeap(Generic[T]):

__slots__ = [

"heap"

heap: List[T]

def __init__(self):

self.heap = []

@staticmethod

def get_parent_index(index: int) -> int:

return (index - 1) // 2

@staticmethod

def get_left_child_index(index: int) -> int:

return 2 * index + 1

@staticmethod

def get_right_child_index(index: int) -> int:

return 2 * index + 2

def has_parent(self, index: int) -> bool:


return self.get_parent_index(index) >= 0

def has_left_child(self, index: int) -> bool:

return self.get_left_child_index(index) < len(self.heap)

def has_right_child(self, index: int) -> bool:

return self.get_right_child_index(index) < len(self.heap)

def parent(self, index: int) -> T:

return self.heap[self.get_parent_index(index)]

def left_child(self, index: int) -> T:

return self.heap[self.get_left_child_index(index)]

def right_child(self, index: int) -> T:

return self.heap[self.get_right_child_index(index)]

def swap(self, index1: int, index2: int) -> None:

self.heap[index1], self.heap[index2] = self.heap[index2], self.heap[index1]

def insert(self, value: T) -> None:

self.heap.append(value)

self.heapify_up()

def heapify_up(self) -> None:

index = len(self.heap) - 1

while self.has_parent(index) and self.parent(index) > self.heap[index]:

self.swap(self.get_parent_index(index), index)

index = self.get_parent_index(index)

def delete_min(self) -> T:

if len(self.heap) == 0:

raise ValueError("Heap is empty")

min_value = self.heap.pop()

self.heapify_down()

return min_value
def heapify_down(self) -> None:

index = 0

while self.has_left_child(index):

smaller_child_index = self.get_left_child_index(index)

if self.has_right_child(index) and self.right_child(index) <


self.left_child(index):

smaller_child_index = self.get_right_child_index(index)

if self.heap[index] < self.heap[smaller_child_index]:

break

self.swap(index, smaller_child_index)

index = smaller_child_index

def empty(self) -> bool:

return len(self.heap) == 0

class Graph:

__slots__ = [

"nodes",

"adj_list",

"edges"

nodes: List[Node]

adk_list: List[Vertex]

edges: List[Edge]

def __init__(self, nodes: List[Node]):

self.nodes = nodes

self.adj_list = [Vertex(node=node) for node in nodes]

self.edges = []

for i in range(len(self.nodes)):

self.nodes[i].index = i

def add_node(self, node: Node) -> None:


node.index = len(self.nodes)

self.nodes.append(node)

self.adj_list.append(Vertex(node=node))

def connect_dir(self, node1: Union[Node, int], node2: Union[Node, int], weight:


float) -> None:

node1_ind, node2_ind = self.get_index_from_node(node1),


self.get_index_from_node(node2)

node1, node2 = self.get_node_from_index(node1_ind),


self.get_node_from_index(node2_ind)

edge = Edge(start=node1, end=node2, weight=weight)

self.adj_list[node1_ind].append(edge)

self.edges.append(edge)

def connect(self, node1: Union[Node, int], node2: Union[Node, int], weight:


float) -> None:

self.connect_dir(node1, node2, weight)

self.connect_dir(node2, node1, weight)

@staticmethod

def get_index_from_node(node: Union[Node, int]) -> int:

if not isinstance(node, Node) and not isinstance(node, int):

raise ValueError("Node must be an integer or a Node object")

if isinstance(node, int):

return node

return node.index

def get_node_from_index(self, index: int) -> Node:

return self.nodes[index]

def get_connections(self, node: Union[Node, int]) -> List[Edge]:

node_ind = self.get_index_from_node(node)

return self.adj_list[node_ind].edges

@timeit

def dijkstra(self, src: Union[Node, int]) -> List[NodeDecorator]:

src = self.get_index_from_node(src)
dnodes = [NodeDecorator(node=node) for node in self.nodes]

dnodes[src].prov_dist = 0

dnodes[src].append_hop(dnodes[src].node)

visited = []

pq = MinHeap[NodeDecorator]()

pq.insert(dnodes[src])

while not pq.empty():

min_decorated_node = pq.delete_min()

min_dist = min_decorated_node.prov_dist

hops = min_decorated_node.hops

visited.append(min_decorated_node.node)

connections = self.get_connections(min_decorated_node.node)

for edge in connections:

if edge.end not in visited:

tot_dist = min_dist + edge.weight

if tot_dist <
dnodes[self.get_index_from_node(edge.end)].prov_dist:

hops_cpy = list(hops)

hops_cpy.append(edge.end)

dnode = dnodes[self.get_index_from_node(edge.end)]

dnode.prov_dist = tot_dist

dnode.hops = hops_cpy

pq.insert(dnode)

return dnodes

@timeit

def bellman_ford(self, src: Union[Node, int]) -> List[NodeDecorator]:

src = self.get_index_from_node(src)

dnodes = [NodeDecorator(node=node) for node in self.nodes]

dnodes[src].prov_dist = 0

dnodes[src].append_hop(dnodes[src].node)

for _ in range(len(dnodes) - 1):


for edge in self.edges:

node1_ind, node2_ind = self.get_index_from_node(edge.start),


self.get_index_from_node(edge.end)

if dnodes[node1_ind].prov_dist + edge.weight <


dnodes[node2_ind].prov_dist:

dnodes[node2_ind].hops = dnodes[node1_ind].hops + [edge.end]

dnodes[node2_ind].prov_dist = dnodes[node1_ind].prov_dist +
edge.weight

for edge in self.edges:

node1_ind, node2_ind = self.get_index_from_node(edge.start),


self.get_index_from_node(edge.end)

if dnodes[node1_ind].prov_dist + edge.weight <


dnodes[node2_ind].prov_dist:

raise ValueError("Negative weight cycle")

return dnodes

class Ui_MainWindow(object):

def setupUi(self, MainWindow):

MainWindow.setObjectName("MainWindow")

MainWindow.resize(1080, 720)

self.centralwidget = QtWidgets.QWidget(parent=MainWindow)

self.centralwidget.setObjectName("centralwidget")

self.label_2 = QtWidgets.QLabel(parent=self.centralwidget)

self.label_2.setGeometry(QtCore.QRect(0, 0, 540, 391))

font = QtGui.QFont()

font.setPointSize(16)

self.label_2.setFont(font)

self.label_2.setAutoFillBackground(False)

self.label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading |
QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)

self.label_2.setIndent(0)

self.label_2.setObjectName("label_2")

self.groupBox = QtWidgets.QGroupBox(parent=self.centralwidget)

self.groupBox.setGeometry(QtCore.QRect(540, 0, 540, 720))

self.groupBox.setObjectName("groupBox")

self.label_6 = QtWidgets.QLabel(parent=self.groupBox)
self.label_6.setGeometry(QtCore.QRect(20, 210, 481, 21))

font = QtGui.QFont()

font.setPointSize(11)

self.label_6.setFont(font)

self.label_6.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)

self.label_6.setObjectName("label_6")

self.label_3 = QtWidgets.QLabel(parent=self.groupBox)

self.label_3.setGeometry(QtCore.QRect(20, 250, 499, 451))

font = QtGui.QFont()

font.setPointSize(10)

self.label_3.setFont(font)

self.label_3.setText("")

self.label_3.setTextFormat(QtCore.Qt.TextFormat.PlainText)

self.label_3.setScaledContents(True)

self.label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|
QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop)

self.label_3.setWordWrap(True)

self.label_3.setObjectName("label_3")

self.layoutWidget = QtWidgets.QWidget(parent=self.groupBox)

self.layoutWidget.setGeometry(QtCore.QRect(21, 51, 491, 150))

self.layoutWidget.setObjectName("layoutWidget")

self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)

self.verticalLayout.setContentsMargins(0, 0, 0, 0)

self.verticalLayout.setObjectName("verticalLayout")

self.label_5 = QtWidgets.QLabel(parent=self.layoutWidget)

self.label_5.setObjectName("label_5")

self.verticalLayout.addWidget(self.label_5)

self.radioButton = QtWidgets.QRadioButton(parent=self.layoutWidget)

self.radioButton.setChecked(True)

self.radioButton.setObjectName("radioButton")

self.verticalLayout.addWidget(self.radioButton)

self.radioButton_2 = QtWidgets.QRadioButton(parent=self.layoutWidget)

self.radioButton_2.setObjectName("radioButton_2")

self.verticalLayout.addWidget(self.radioButton_2)

self.label_4 = QtWidgets.QLabel(parent=self.layoutWidget)

self.label_4.setObjectName("label_4")

self.verticalLayout.addWidget(self.label_4)
self.comboBox = QtWidgets.QComboBox(parent=self.layoutWidget)

self.comboBox.setObjectName("comboBox")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.comboBox.addItem("")

self.verticalLayout.addWidget(self.comboBox)

self.pushButton = QtWidgets.QPushButton(parent=self.layoutWidget)

self.pushButton.setObjectName("pushButton")

self.verticalLayout.addWidget(self.pushButton)

self.label = QtWidgets.QLabel(parent=self.centralwidget)

self.label.setGeometry(QtCore.QRect(0, 360, 540, 360))

self.label.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft)

self.label.setText("")

self.label.setPixmap(QtGui.QPixmap("graph.png"))

self.label.setScaledContents(True)

self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)

self.label.setObjectName("label")

MainWindow.setCentralWidget(self.centralwidget)

self.retranslateUi(MainWindow)

QtCore.QMetaObject.connectSlotsByName(MainWindow)

def retranslateUi(self, MainWindow):

_translate = QtCore.QCoreApplication.translate

MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))

self.label_2.setText(_translate("MainWindow", "Вершини графу:\n"

"1. Червоний університет\n"

"2. Андріївська церква\n"

"3. Михайлівський собор\n"


"4. Золоті ворота\n"

"5. Лядські ворота\n"

"6. Фунікулер\n"

"7. Київська політехніка\n"

"8. Фонтан на Хрещатику\n"

"9. Софія київська\n"

"10. Національна філармонія\n"

"11. Музей однієї вулиці\n"

""))

self.groupBox.setTitle(_translate("MainWindow", "Пошук найдешевшого шляху


від КПІ"))

self.label_6.setText(_translate("MainWindow", "Вивід програми"))

self.label_5.setText(_translate("MainWindow", "Оберіть алгоритм:"))

self.radioButton.setText(_translate("MainWindow", "Алгоритм Дейкстри"))

self.radioButton_2.setText(_translate("MainWindow", "Алгоритм Беллмана-


Форда"))

self.label_4.setText(_translate("MainWindow", "Оберіть кінцеву точку:"))

self.comboBox.setItemText(0, _translate("MainWindow", "Червоний


університет"))

self.comboBox.setItemText(1, _translate("MainWindow", "Андріївська церква"))

self.comboBox.setItemText(2, _translate("MainWindow", "Михайлівський


собор"))

self.comboBox.setItemText(3, _translate("MainWindow", "Золоті ворота"))

self.comboBox.setItemText(4, _translate("MainWindow", "Лядські ворота"))

self.comboBox.setItemText(5, _translate("MainWindow", "Фунікулер"))

self.comboBox.setItemText(6, _translate("MainWindow", "Київська


політехніка"))

self.comboBox.setItemText(7, _translate("MainWindow", "Фонтан на


Хрещатику"))

self.comboBox.setItemText(8, _translate("MainWindow", "Софія київська"))

self.comboBox.setItemText(9, _translate("MainWindow", "Національна


філармонія"))

self.comboBox.setItemText(10, _translate("MainWindow", "Музей однієї


вулиці"))

self.pushButton.setText(_translate("MainWindow", "Виконати"))

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):

def __init__(self, *args, **kwargs):

super(MainWindow, self).__init__(*args, **kwargs)


self.start = None

self.graph = None

self.setupUi(self)

self.setWindowTitle("Теорія Алгоритмів")

self.setWindowIcon(QIcon('neural.png'))

self.create_graph()

self.setFixedSize(1080, 720)

self.label_2.setStyleSheet("margin-left: 15px;")

self.pushButton.clicked.connect(self.click)

self.dijkstra_list = None

self.bellman_ford_list = None

def create_graph(self):

a = Node(data="Червоний університет")

b = Node(data="Андріївська церква")

c = Node(data="Михайлівський собор")

d = Node(data="Золоті ворота")

e = Node(data="Лядські ворота")

f = Node(data="Фунікулер")

g = Node(data="Київська політехніка")

h = Node(data="Фонтан на Хрещатику")

i = Node(data="Софія київська")

j = Node(data="Національна філармонія")

k = Node(data="Музей однієї вулиці")

self.graph = Graph([a, b, c, d, e, f, g, h, i, j, k])

self.start = g

self.graph.connect(g, a, 8)

self.graph.connect(g, d, 8)

self.graph.connect(a, d, 8)

self.graph.connect(g, h, 8)

self.graph.connect(a, h, 8)

self.graph.connect(h, e, 8)

self.graph.connect(h, j, 8)
self.graph.connect(j, e, 8)

self.graph.connect(h, f, 8)

self.graph.connect(h, k, 8)

self.graph.connect(d, i, 14)

self.graph.connect(i, c, 6)

self.graph.connect(i, b, 8)

self.graph.connect(b, c, 6)

def click(self):

index = self.comboBox.currentIndex()

end = self.graph.get_node_from_index(index)

if self.radioButton.isChecked():

self.dijkstra_list = self.graph.dijkstra(self.start)

distances = self.dijkstra_list

else:

self.bellman_ford_list = self.graph.bellman_ford(self.start)

distances = self.bellman_ford_list

distance = distances[index]

route = ""

for i, node in enumerate(distance.hops):

route += f"{i + 1}. {node.data}({node.index + 1})\n"

self.label_3.setText(f"Ціна проїзду від '{self.start}' до '{end}' -


{distance.prov_dist} гривень\nМаршрут:\n{route}")

app = QApplication(sys.argv)

window = MainWindow()

window.show()

app.exec()

Перевірка правильності
Перевірка правильності роботи алгоритму
Результат Призначення тексту
Вхідні дані
radioButton comboBox
1 Михайлівській Відомий Загальна перевірка
собор працездатності алгоритму та
коректності результату
2 Музей однієї Відомий Загальна перевірка
вулиці працездатності алгоритму та
коректності результату

Перевірка графічного інтерфейсу

Результат Призначення тексту

Вхідні дані
MainWindow Відомий Перевірка відображення графічного інтерфейсу

radioButton1 Відомий Загальна перевірка працездатності графічного


інтерфейсу

pushButton Відомий Загальна перевірка працездатності графічного


інтерфейсу

Скріншоти роботи програми


Маршрут від КПІ до Михайлівського собору за алгоритмом Дейксти
Маршрут від КПІ до Музею однієї вулиці за алгоритмом Беллмана Форда

Результати досліджень у вигляді графіку:


Алгоритм Дейкстри O(n*log(n))

Алгоритм Беллмана-Форда O(n^2)

Відповіді на контрольні питання


1. Перерахуйте відомі вам методи розробки алгоритмів. Докладніше
розкажіть про один з них.

● Метод роздільного опису - алгоритмічний підхід, який заснований


на розбитті складної задачі на менші підзадачі та їх рекурсивному
розв'язанні. Кожна підзадача є самостійною задачею і може бути
розв'язана окремо від інших. Після розв'язання всіх підзадач, їх
рішення комбінуються для отримання рішення вихідної задачі.
Метод роздільного опису дозволяє розв'язувати складні задачі за
менший час, порівняно зі звичайними рекурсивними алгоритмами.
Однак, існують також обмеження на застосування цього методу.
Наприклад, не завжди можна ефективно розділити задачу на менші
підзадачі, запросто може виникнути проблема зі збільшенням об'єму
пам'яті, якщо підзадачі надто маленькі, а їх кількість велика. Тому
цей спосіб не варто використовувати бездумно, як вирішення всіх
проблем.
● Метод зворотного ходу - алгоритм числового розв'язання систем
лінійних рівнянь з квадратною матрицею. Він зазвичай
використовується для знаходження вектора невідомих змінних,
якщо відомі їх лінійні комбінації. Цей вид алгоритму містить 3
етапи:

Прямий хід: від оригінальної системи рівнянь до системи верхньої


трикутної матриці.

Обернений хід: від системи верхньої трикутної матриці до системи


діагональних рівнянь.

Знаходження невідомих змінних: знаходження кожної змінної


шляхом підстановки знайденого значення у попередні рівняння.

Саме цей метод ефективний для матриць з великою кількістю рядків


та стовпців. Варто згадати і те, що цей метод дозволяє розв'язувати
системи лінійних рівнянь з точністю до машинного нуля, що дає
певні підстави для довіри цьому методу.

● Метод динамічного програмування - спосіб розв'язання складних


задач, що базуються на рекурсивних викликах, та мають багато
повторюваних підзадач. Цей метод полягає в тому, щоб розв'язати
складну задачу, розбивши її на менші прості задачі, та зберігаючи
результати їх виконання для подальшого використання. Сюди
входять такі пункти: Визначення оптимальної структури проблеми,
розбиття проблеми на підзадачі, визначення рекурсивного
відношення між підзадачами, розроблення алгоритму з обчисленням
рішення для кожної підзадачі, збереження результатів розв'язку
кожної підзадачі, побудова оптимального розв'язку з результатів
підзадач. Варто зазначити, що цей спосіб чудово підходить для
вирішення графів, якраз для завдання на сьогодні (неймовірний
збіг 🙂)

● Метод грубої сили - примітивний алгоритм розв'язування задачі


шляхом перебору всіх можливих варіантів. Нескладно здогадатися,
що основна ідея методу полягає в тому, щоб перебрати всі можливі
варіанти розв'язку задачі, зазвичай для цього використовують цикли
або рекурсії. Та цей метод буде дуже часо та ресурсомістким,
особливо якщо маємо велику кількість можливих варіантів розв'язку
задачі. Тому він не завжди є ефективним методом розв'язання
складних задач, але за відсутності інших варіантів, цей спосіб
звучить не так погано, правда ж?
● Метод жадібного алгоритму - алгоритм розв'язання оптимізаційних
задач, який вирішує кожен етап задачі, вибираючи оптимальне
рішення на кожному кроці. Цей метод припускає, що оптимальне
рішення на поточному етапі забезпечує найкращий результат на
наступному етапі, тобто веде до оптимального розв'язку всієї задачі.

Наприклад, якщо будемо брати для прикладу наше завдання


потрібно знайти маршрут, який проходить через кілька точок і має
найменшу вартість, то жадібний алгоритм може вибрати найближче
місто на кожному кроці, без урахування загальної вартості
маршруту.

Жадібний алгоритм може допомогти зменшити час


виконання задачі, оскільки він працює швидше за
інші методи розв'язання задач. Однак, він не
завжди дає оптимальний розв'язок задачі і може
привести до недооцінки деяких факторів або до
відкидання деяких можливих варіантів. Недоліком
варто вважати й локальний характер алгоритму,
тобто він може знайти оптимальний результат лише
на поточному етапі, а не на всій задачі в цілому.
Тому для деяких задач можуть існувати кращі
методи розв'язання, які забезпечують глобальну
оптимальність. (Цей спосіб теж можна позначити
зірочкою, бо він, певною мірою, підходить до
нашого завдання ⭐)
● Метод хешування - це метод зберігання та пошуку даних в
структурах даних, що базується на принципі використання хеш-
функцій. Хеш-функція приймає на вхід довільний великий об'єкт
даних, такий як рядок або масив, і повертає випадкову величину
фіксованої довжини, що називається хеш-значенням.

При додаванні нового об'єкту до хеш-таблиці, його хеш-значення


обчислюється, а потім об'єкт додається до списку з іншими
об'єктами, які мають те ж саме хеш-значення. При пошуку об'єкта в
хеш-таблиці, його хеш-значення також обчислюється, і знаходиться
список об'єктів з тим же хеш-значенням. Потім з цього списку
шукається сам об'єкт.

Метод хешування може значно зменшити час пошуку та додавання


об'єктів в структури даних, особливо при великих об'ємах даних.
Проте, він може бути ненадійним, якщо хеш-функція дає колізії,
тобто два різних об'єкти мають одне і те ж хеш-значення. Це може
призвести до пошуку неправильного об'єкта або до збою в системі.
Тому важливо вибрати хорошу хеш-функцію і використовувати
метод хешування з обробкою колізій, щоб забезпечити правильну
роботу системи.

● Метод віток та меж - алгоритм, який використовується для пошуку


оптимального рішення в задачах комбінаторної оптимізації. Він
застосовується в тих випадках, коли не можна знайти оптимальне
рішення за допомогою методів динамічного програмування або
жадібних алгоритмів.

Метод віток та меж є комбінацією двох методів: методу віток, який


відповідає за розбиття задачі на менші підзадачі, та методу меж,
який відповідає за відкидання непотрібних варіантів. (І сюди
поставимо зірочку 🙂⭐ 🫴⭐ )

● Метод еволюційних алгоритмів - алгоритм, який моделює


природний процес еволюції для пошуку оптимального рішення в
задачах оптимізації. Цей метод заснований на ідеї, що кращі рішення
мають більші шанси на виживання та розмноження, тому вони
мають бути збережені та посилені в наступному поколінні.
(Майже, як у біології)

Притаманними етапами цього способу є:

1. Створення початкової популяції. Починається з генерації випадкової


популяції, яка складається з рішень. Кожен елемент популяції
представляється у вигляді вектора, який містить значення
параметрів, що визначають рішення.
2. Оцінка пристосованості. Кожен елемент популяції оцінюється за
його пристосованість до вирішення задачі оптимізації. Ця оцінка
відображає, наскільки близько елемент до оптимального рішення.
3. Відбір. Вибираються кращі елементи популяції для подальшого
розмноження. Цей процес може бути здійснений згідно з різними
правилами, наприклад, випадково, або ж за допомогою елітарного
відбору, коли кращі елементи зберігаються для наступного
покоління.
4. Розмноження. Відбувається шляхом комбінування генетичної
інформації батьків для створення нових рішень. Цей процес може
бути реалізований за допомогою різних методів, наприклад,
кросоверу, мутації та інших.
5. Оцінка пристосованості нової популяції. Нова популяція оцінюється
за її пристосованість до вирішення задачі оптимізації. Цей етап
повторюється для кожного нового покоління, щоб забезпечити
поступове покращення популяції та наближення до оптимального
рішення.

(Стосовно проміжних версій вам варто звернутися до нашого

тімліда Миколи Красовського 🫡)

2. Перерахуйте переваги та недоліки наступних методів розробки


алгоритмів: методу часткових (проміжних) цілей, методу підйому
(локального пошуку), методу відпрацювання назад.

Метод часткових (проміжних) цілей:

Переваги:
● Дозволяє розбити складну задачу на менші частини, що

полегшує розробку та тестування алгоритму

● Підвищує ймовірність успішного вирішення задачі,

оскільки рішення кожної окремої частини може бути

оптимальним

Недоліки:

● Виникає ризик недостатньої уваги до взаємозв 'язків

між частинами, що може призвести до не оптимального

рішення;

● Значний час може бути витрачений на розробку та

інтеграцію різних частин, що зменшує продуктивність

розробки

Метод підйому (локального пошуку):

Переваги:

● Швидко знаходить оптимальне рішення в малих

областях простору вхідних даних;

● Є ефективним для оптимізації функцій, які мають одну

або кілька глобальних мінімумів.

Недоліки:
● Є вразливим до попадання в локальні мінімуми, що

може призвести до не оптимального рішення

● Потребує початкового значення параметрів, що може

бути важко визначити в складних задачах

● Не гарантує знаходження глобального мінімуму

Метод відпрацювання назад:

Переваги:

● Дозволяє розбити задачу на послідовні етапи, що

полегшує розробку та тестування алгоритму

● Дозволяє зменшити ризик помилок, оскільки кожен етап

може бути перевірений окремо

● Дозволяє розв'язати складну задачу, яка не може бути

розв'язана за один крок

Недоліки:

● Може призвести до підвищення складності алгоритму,

оскільки потребує збереження проміжних результатів на

кожному етапі

3. Який тип алгоритмів називають «жадібними» і чому?

Опис цього алгоритму знаходиться у запитанні 1.

Жадібні алгоритми називають "жадібними" через їхню основну


стратегію прийняття рішень - на кожному кроці алгоритму, він
вибирає локально найкращий варіант без урахування можливих
наслідків майбутніх кроків.

(Та особисто, я не розумію, чому його так назвали. Йому б пасувала


назва “Егоїстичний” алгоритм)

4. Дайте характеристику евристичним алгоритмам. В яких випадках


доцільно використовувати цей тип алгоритмів? Опишіть загальний
підхід до побудови евристичних алгоритмів.
Евристичні алгоритми - алгоритми, які шукають швидкі та
практичні рішення складних проблем, незважаючи на те, що їхні рішення
можуть не бути оптимальними. Їхня робота базується на принципі
поступового покращення рішення шляхом перевірки та зміни його на
кожному кроці.

Доцільно використовувати евристичні алгоритми у випадках, коли не


можна точно знайти оптимальне рішення, оскільки воно є занадто
складним для розв'язання або його розв'язання потребує занадто багато
часу. Наприклад, евристичні алгоритми використовуються в задачах
маршрутизації, планування розкладів та в інших галузях, де необхідно
швидко знайти прийнятні рішення.

Загальний підхід до побудови евристичних алгоритмів включає наступні


етапи:

● Вибір метрики оцінки - визначення критеріїв, за якими будуть


оцінюватися рішення.
● Розробка ідеї алгоритму - визначення способу пошуку
оптимального рішення на основі метрики оцінки.
● Розробка евристичного правила - визначення критеріїв, які
дозволяють вибирати найбільш перспективні рішення.
● Розробка процедури пошуку - реалізація евристичного правила та
визначення послідовності операцій, які необхідно виконати для
знаходження оптимального рішення.
● Тестування та оптимізація - перевірка алгоритму на різних
тестових прикладах та вдосконалення алгоритму за результатами
тестування.
5. Проаналізуйте, що спільного мають та чим відрізняються
алгоритми, що використовують пошук з поверненням, та алгоритми,
що використовують метод гілок та границь.
Якщо говорити про спільне, то і алгоритми з пошуком з
поверненням, і алгоритми з методом гілок та границь
використовуються для пошуку оптимального рішення в проблемах зі
складною структурою та великою кількістю можливих варіантів
рішення.

Однак, головною відмінністю між ними є те, що алгоритми з


пошуком з поверненням перебирають всі можливі варіанти рішення
без будь-якої перед попередньої оптимізації, що може забрати
багато часу та ресурсів. Такі алгоритми зазвичай використовуються
для пошуку оптимального рішення в простих задачах, де кількість
можливих варіантів рішення невелика.

У свою чергу, алгоритми з методом гілок та границь


використовуються для вирішення складних задач, де кількість
можливих варіантів рішення дуже велика. Вони зазвичай
ґрунтуються на деяких евристичних або математичних техніках, які
дозволяють обмежувати кількість перебирань варіантів, зменшуючи
час та ресурси, що витрачаються на пошук.

Ще одна відмінність полягає в тому, що алгоритми з методом гілок


та границь зазвичай використовуються для пошуку глобального
оптимального рішення, тоді як алгоритми з пошуком з поверненням
можуть знайти лише локальне оптимальне рішення.

Отже, хоча алгоритми з пошуком з поверненням та алгоритми з


методом гілок та границь можуть застосовуватися для вирішення
схожих проблем, їх підходи та результати можуть значно
відрізнятися в залежності від складності задачі та характеристик
даних.

6. Поясніть, для чого можна використовувати метод альфа-бета


відсікання.
Метод альфа-бета відсікання - це алгоритм, який використовується в
програмах для зменшення кількості перебору варіантів та
покращення швидкості пошуку оптимального рішення. Застосування
цього методу дає змогу ефективно обрізати гілки дерева пошуку, які
не призводять до покращення поточного кращого рішення.
7. Поясніть термін «структурне програмування». Для чого воно
застосовується?
Структурне програмування це підхід до програмування, який
базується на використанні структури програм та контрольованого
переходу між ними. Основною ідеєю структурного програмування є
розбиття складних задач на більш прості частини (щось схоже на
методи роздільного опису), які можна легко реалізувати з
використанням стандартних конструкцій програмування, таких як
послідовність, розгалуження та цикли.

Структурне програмування застосовується для поліпшення


структури програм, зробити їх більш зрозумілими, легкими для
розуміння, підтримки та модифікації. Це дозволяє писати програми
більш ефективно та ефективно керувати складністю коду. Крім того,
структурне програмування сприяє підвищенню якості програм,
покращенню їх надійності та зменшенню кількості помилок.
(Думаю, можна не пояснювати важливість терміну. Але, якщо ви не
розумієте значення цього терміну, то роботодавці вам все
пояснять 😏 )

Висновок
Виконуючи практикум № 2 з дисципліни - Теорія алгоритмів на
тему “Методи розробки алгоритмів. Частина 1”, ми використовували
Python для програмування. Оскільки ця мова програмування
зарекомендувала себе, як простий та багатофункціональний
інструмент, який ще й можна легко модифікувати.
Аби виконати практикум ми мали обрати певні алгоритми. Наш
вибір впав на алгоритми Беллмана-Форда та Дейкстри. Саме їхні
принципи ми відтворювали за допомогою пайтону для вирішення
нашого завдання. Як результат - ми змогли достовірно отримати всі
можливі шляхи та їх ціну, аби “спланувати” прогулянку.

Варто зазначити, що за результатами досліджень алгоритм Дейкстри


виявився швидше O(E*Log(V)) так як він використовує жадібну
стратегію і перебирає ребра, які виходять із поточної вершини, а
алгоритм Беллмана Форда використовує прямий перебір усіх ребер
V разів O(V*E), однак на відміну від алгоритму Дейкстри він може
працювати з від’ємними ребрами.

Отже, якщо маємо граф з невід'ємними вагами ребер, то алгоритм


Дейкстри з використанням мінімальної купи буде ефективнішим за
алгоритм Беллмана-Форда. Але якщо маємо граф з від'ємними
вагами ребер, то нам потрібно використовувати алгоритм Беллмана-
Форда.

Виконуючи цю роботу (а саме: розбираючи та відповідаючи на


запитання), ми також ознайомилися з великою кількістю методів для
вирішення поставлених завдань, від примітивного методу грубої
сили, де просто перебираються всі можливі варіанти
до методу еволюційних алгоритмів, який взагалі є напрямом у
штучному інтелекті, що використовує і моделює біологічну
еволюцію.

Варто згадати й певні професійні терміни, які зустрілися нам у цій


роботі, наприклад: метод альфа-бета відсікання та структурне
програмування. Їхнє значення розписане у запитаннях 6. та 7., якщо
ви плануєте бути професіоналом своєї справи (у галузі ІТ), то ці
терміни є обов’язковими для ознайомлення.

Дякую за увагу.
Слава Україні!

You might also like