├── Based_on_Grokking_algorithms.ipynb └── README.md /Based_on_Grokking_algorithms.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## На базе разбора книги \"Грокаем алгоритмы\". Разобрано больше, потому как в книге информации мало и многие вещи разобраны довольно слабо." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### 1. Бинарный поиск\n", 15 | "- Массив должен быть отсортирован\n", 16 | "- Сложность $O(log_2n)$" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 77, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "def binary_search(data: list, question: int) -> int:\n", 26 | " low = 0 # инициализируем левую границу\n", 27 | " high = len(data)-1 # инициализируем правую границу\n", 28 | " while low <= high: # до тех пор, пока левая граница меньше или равна правой\n", 29 | " mid = (high+low)//2 # делим пополам, выбираем центральный элемент\n", 30 | " if question == data[mid]: # если совпало - выходим\n", 31 | " return mid\n", 32 | " elif data[mid] > question: # если центральнй элемент больше, уменьшаем правую границу\n", 33 | " high = mid - 1\n", 34 | " else: # если центральный элемент меньше, увеличиваем левую границу.\n", 35 | " low = mid + 1 \n", 36 | " return None # если ничего не нашлось, возвращаем None" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 78, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stdout", 46 | "output_type": "stream", 47 | "text": [ 48 | "['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']\n", 49 | "У буквы j индекс - 9\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "import string\n", 55 | "test_array = [letter for letter in string.ascii_lowercase]\n", 56 | "print(test_array)\n", 57 | "print('У буквы j индекс -', binary_search(test_array, 'j'))" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "### 2. Сортировка пузырьком.\n", 65 | "- Сложность $O(n^2)$\n", 66 | "- Используется только как обучающий.\n", 67 | "- Есть улучшающие модификации" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 87, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "def bubble_sort(data: list) -> list:\n", 77 | " for i in range(len(data)-1): # Внешний цикл, n-1 проходов.\n", 78 | " for j in range(len(data)-i-1): # Внутренний цикл. n-i-1 сравнений.\n", 79 | " if data[j] > data[j+1]: \n", 80 | " data[j], data[j+1] = data[j+1], data[j] # каждая пара сортируется по возрастанию\n", 81 | " return data" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 88, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "name": "stdout", 91 | "output_type": "stream", 92 | "text": [ 93 | "[32, 42, 26, 0, 49, 29, 35, 45, 8, 40, 43, 10, 41, 22, 31, 18, 5, 47, 37, 17, 11, 38, 9, 20, 1, 36, 3, 12, 15, 6, 44, 28, 33, 34, 30, 4, 16, 7, 24, 19, 46, 48, 25, 13, 2, 23, 27, 14, 39, 21]\n", 94 | "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]\n" 95 | ] 96 | } 97 | ], 98 | "source": [ 99 | "import random\n", 100 | "bs_test_array = [i for i in range(50)]\n", 101 | "random.shuffle(bs_test_array)\n", 102 | "print(bs_test_array)\n", 103 | "print(bubble_sort(bs_test_array))" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 89, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "# Можно немного сократить количество операций, проверяя были ли перестановки.\n", 113 | "# И если перестановок в цикле не было ни одной - завершать работу.\n", 114 | "\n", 115 | "def better_bubble_sort(data: list):\n", 116 | " n = len(data)\n", 117 | " swapped = True # Флаг, были ли перестановки.\n", 118 | " i = -1 # Счетчик для внутреннего цикла\n", 119 | " \n", 120 | " while swapped: # если перестановки были, продолжаем цикл\n", 121 | " i +=1\n", 122 | " swapped = False\n", 123 | " for j in range(n-i-1):\n", 124 | " if data[j] > data[j+1]:\n", 125 | " data[j],data[j+1] = data[j+1],data[j]\n", 126 | " swapped = True\n", 127 | " return data" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 90, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "name": "stdout", 137 | "output_type": "stream", 138 | "text": [ 139 | "[15, 27, 21, 40, 0, 23, 6, 17, 25, 36, 9, 31, 11, 2, 32, 37, 5, 38, 49, 1, 16, 33, 20, 18, 45, 42, 8, 19, 3, 41, 28, 12, 26, 22, 14, 44, 48, 39, 43, 35, 34, 24, 13, 29, 30, 10, 4, 47, 46, 7]\n", 140 | "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]\n" 141 | ] 142 | } 143 | ], 144 | "source": [ 145 | "bs_test_array = [i for i in range(50)]\n", 146 | "random.shuffle(bs_test_array)\n", 147 | "print(bs_test_array)\n", 148 | "print(better_bubble_sort(bs_test_array))" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 92, 154 | "metadata": {}, 155 | "outputs": [ 156 | { 157 | "name": "stdout", 158 | "output_type": "stream", 159 | "text": [ 160 | "13.8 s ± 59.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 161 | ] 162 | } 163 | ], 164 | "source": [ 165 | "%%timeit\n", 166 | "test_array = list(range(10000))\n", 167 | "random.shuffle(test_array)\n", 168 | "better_bubble_sort(test_array)" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "### 3. Сортировка выбором.\n", 176 | "- Быстрее пузырьковой сортировки, поскольку смена позиций элементов происходит только 1 раз во внутреннем цикле\n", 177 | "- Всё так же нестабильна как и пузырьковая\n", 178 | "- Сложность также $O(n^2)$ в худшем случае" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 94, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "def selection_sort(data: list) -> list:\n", 188 | " for i in range(len(data)):\n", 189 | " min_idx = i\n", 190 | " for j in range(i+1, len(data)): # находим индекс минимального элемента в списке\n", 191 | " if data[j] < data[min_idx]:\n", 192 | " min_idx = j\n", 193 | " data[i],data[min_idx] = data[min_idx],data[i] # меняем текущий элемент с минимальным местами\n", 194 | " return data" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 95, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "name": "stdout", 204 | "output_type": "stream", 205 | "text": [ 206 | "5.56 s ± 31.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 207 | ] 208 | } 209 | ], 210 | "source": [ 211 | "%%timeit\n", 212 | "test_array = list(range(10000))\n", 213 | "random.shuffle(test_array)\n", 214 | "selection_sort(test_array)" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### 4. Быстрая сортировка\n", 222 | "- Рекурсивный алгоритм\n", 223 | "- Худшее время $O(n^2)$\n", 224 | "- Среднее время, оно же лучшее $O(n*log n)$" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": 96, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "def quick_sort(data: list) -> list:\n", 234 | " if len(data) < 2: # Базовый случай. Длина массива меньше двух.\n", 235 | " return data\n", 236 | " else:\n", 237 | " pivot = data.pop(random.randint(0,len(data)-1)) # выбираем случайный опорный элемент\n", 238 | " lesser = [elem for elem in data if elem < pivot] # массив элементов меньше опорного\n", 239 | " greater = [elem for elem in data if elem >= pivot] # массив элементов больше опорного\n", 240 | " return quick_sort(lesser) + [pivot] + quick_sort(greater) # дважды рекурсивно вызываем сортировку и конкатенируем результаты с опорным элементом." 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": 97, 246 | "metadata": {}, 247 | "outputs": [ 248 | { 249 | "name": "stdout", 250 | "output_type": "stream", 251 | "text": [ 252 | "46.6 ms ± 951 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 253 | ] 254 | } 255 | ], 256 | "source": [ 257 | "%%timeit\n", 258 | "test_array = list(range(10000))\n", 259 | "random.shuffle(test_array)\n", 260 | "quick_sort(test_array)" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "metadata": {}, 266 | "source": [ 267 | "#### 4a. Быстрая сортировка без использования дополнительной памяти. С разбиением Хоара." 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": 98, 273 | "metadata": {}, 274 | "outputs": [], 275 | "source": [ 276 | "def true_quicksort(data: list):\n", 277 | " # пока размер массива больше 1 разбиваем массив на части и рекурсивно применяем эту же функцию\n", 278 | " def hoar_sort(data: list, low: int, high: int):\n", 279 | " if low < high:\n", 280 | " pivot = partition(data, low, high)\n", 281 | " hoar_sort(data, low, pivot)\n", 282 | " hoar_sort(data, pivot+1, high)\n", 283 | " \n", 284 | " # разбиене Хоара. Двигаемся с двух сторон, меняем местами элементы больше и меньше опорного.\n", 285 | " # возвращаем индекс опорного элемента в пересортированном массиве \n", 286 | " def partition(data: list, low, high):\n", 287 | " pivot = data[random.randint(low,high)]\n", 288 | " \n", 289 | " while True:\n", 290 | " while data[low] < pivot:\n", 291 | " low += 1\n", 292 | " while data[high] > pivot:\n", 293 | " high -= 1\n", 294 | " if low >= high:\n", 295 | " return low\n", 296 | " data[low],data[high] = data[high],data[low]\n", 297 | " \n", 298 | " hoar_sort(data, 0, len(data)-1)" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": 99, 304 | "metadata": {}, 305 | "outputs": [ 306 | { 307 | "name": "stdout", 308 | "output_type": "stream", 309 | "text": [ 310 | "77.9 ms ± 1.76 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 311 | ] 312 | } 313 | ], 314 | "source": [ 315 | "%%timeit\n", 316 | "test_array = list(range(10000))\n", 317 | "random.shuffle(test_array)\n", 318 | "true_quicksort(test_array)" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "### 5. Сортировка вставками\n", 326 | "- Временная сложность $O(n^2)$" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": 100, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "def insertion_sort(data: list):\n", 336 | " # цикл от 1 до конца массива.\n", 337 | " for i in range(1,len(data)):\n", 338 | " # сохраняем ключевой элемент(каждый раз новый)\n", 339 | " key = data[i]\n", 340 | " j = i-1 # смещаемся на 1 элемент назад\n", 341 | " # далее поочерёдно проверяем элементы от текущего положения\n", 342 | " # до начала массива. Если элемент больше ключевого, то смещаем его на 1 позицию вправо.\n", 343 | " # после этого уменьшаем индекс и проверяем следующий элемент. Если и он больше ключевого,\n", 344 | " # смещаем его тоже на 1 позицию вправо. И так до тех пор, пока не наткнемся на элемент меньше или равный ключевому,\n", 345 | " # или доберёмся до начала массива. \n", 346 | " # Таким образом мы последовательно смещаем весь массив со значениями больше ключевого - вправо.\n", 347 | " while (j >= 0) and (data[j] > key):\n", 348 | " data[j+1] = data[j]\n", 349 | " j -=1\n", 350 | " # и наконец вставляем ключевой элемент на нужную позицию (элемент слева - меньше или равен ключевому, элементы справ - больше)\n", 351 | " data[j+1] = key" 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 101, 357 | "metadata": {}, 358 | "outputs": [ 359 | { 360 | "name": "stdout", 361 | "output_type": "stream", 362 | "text": [ 363 | "7.01 s ± 31.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n" 364 | ] 365 | } 366 | ], 367 | "source": [ 368 | "%%timeit\n", 369 | "test_array = list(range(10000))\n", 370 | "random.shuffle(test_array)\n", 371 | "insertion_sort(test_array)" 372 | ] 373 | }, 374 | { 375 | "cell_type": "markdown", 376 | "metadata": {}, 377 | "source": [ 378 | "### 6. Сортировка слиянием\n", 379 | " - временная сложность $O(n\\cdot logn)$\n", 380 | " - константы больше чем у quicksort, поэтому работает в среднем чуть медленнее\n", 381 | " - требует дополнительной памяти, столько же, сколько занимает исходный массив\n", 382 | " - спокойно работае с данными, поддерживающими только последовательный доступ(связанные списки)" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 102, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "def merge_sort(data: list):\n", 392 | " # Базовый случай, массив размера меньше 2 уже отсортирован.\n", 393 | " if len(data) < 2:\n", 394 | " return data\n", 395 | " # Делим массив на две примерно равные части\n", 396 | " L = merge_sort(data[:len(data) // 2])\n", 397 | " R = merge_sort(data[len(data) // 2:]) \n", 398 | " # Переменные для индексов правого, левого и результирующего массивов\n", 399 | " n = m = k = 0\n", 400 | " # Создаём результирующий массив, такого же размера, как исходный.\n", 401 | " C = [0] * (len(L) + len(R))\n", 402 | " # Непосредственно само слияние массивов\n", 403 | " while n < len(L) and m < len(R): # пока не пройден хотя бы один из двух массивов полностью.\n", 404 | " # Сравниваем элементы двух массивов.\n", 405 | " # Меньший из них записываем в результирующий массив. \n", 406 | " # Увеличиваем индекс этого подмассива и результирующего массива на единицу\n", 407 | " if L[n] <= R[m]:\n", 408 | " C[k] = L[n]\n", 409 | " n += 1\n", 410 | " else:\n", 411 | " C[k] = R[m]\n", 412 | " m += 1\n", 413 | " k += 1\n", 414 | " # Прикрепляем \"остатки\"\n", 415 | " # Если любой из подмассивов закончился, а другой ещё нет,\n", 416 | " # Добавляем все элементы оставшегося подмассива в результирующий.\n", 417 | " while n < len(L):\n", 418 | " C[k] = L[n]\n", 419 | " n += 1\n", 420 | " k += 1\n", 421 | " while m < len(R):\n", 422 | " C[k] = R[m]\n", 423 | " m += 1\n", 424 | " k += 1\n", 425 | " # Переписываем исходный массив и возвращаем его.\n", 426 | " for i in range(len(data)):\n", 427 | " data[i] = C[i]\n", 428 | " return data" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 103, 434 | "metadata": {}, 435 | "outputs": [ 436 | { 437 | "name": "stdout", 438 | "output_type": "stream", 439 | "text": [ 440 | "98 ms ± 852 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" 441 | ] 442 | } 443 | ], 444 | "source": [ 445 | "%%timeit\n", 446 | "test_array = list(range(10000))\n", 447 | "random.shuffle(test_array)\n", 448 | "merge_sort(test_array)" 449 | ] 450 | }, 451 | { 452 | "cell_type": "markdown", 453 | "metadata": {}, 454 | "source": [ 455 | "### 7. Timsort.\n", 456 | "- Объединяет сортировку вставками и сортировку слиянием\n", 457 | "- Реализация сложная, разберу позже. Будет мне челленджем.\n", 458 | "- Основная идея - поиск уже отсортированных подмассивов в исходном массиве. Логично предполагается, что в реальной жизни большинство массивов, требующих сортировки, содержат в себе большое количество уже отсортированных подмассивов, и на их сортировку не нужно тратить времени. Можно сразу слияние осуществлять." 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "## Графы" 466 | ] 467 | }, 468 | { 469 | "cell_type": "markdown", 470 | "metadata": {}, 471 | "source": [ 472 | "### 8. Поиск в ширину. (Breadth-Frist Search)\n", 473 | "- Последовательно проходит по всем соседям исходного узла, затем по соседям каждого из них и т.д.\n", 474 | "- используется очередь. LIFO. В python удобно deque из collections использовать.\n", 475 | "- Сложность $O(V+E)$, где V - число вершин(узлов), E - число рёбер\n", 476 | " " 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 104, 482 | "metadata": {}, 483 | "outputs": [], 484 | "source": [ 485 | "import graphviz\n", 486 | "from collections import deque" 487 | ] 488 | }, 489 | { 490 | "cell_type": "code", 491 | "execution_count": 105, 492 | "metadata": {}, 493 | "outputs": [ 494 | { 495 | "data": { 496 | "text/plain": [ 497 | "{'you': ['alice', 'bob', 'claire'],\n", 498 | " 'bob': ['anuj', 'peggy', 'you'],\n", 499 | " 'alice': ['peggy', 'you'],\n", 500 | " 'claire': ['thom', 'jonny', 'you'],\n", 501 | " 'anuj': ['bob'],\n", 502 | " 'peggy': ['bob', 'alice'],\n", 503 | " 'thom': ['claire'],\n", 504 | " 'jonny': ['claire']}" 505 | ] 506 | }, 507 | "execution_count": 105, 508 | "metadata": {}, 509 | "output_type": "execute_result" 510 | } 511 | ], 512 | "source": [ 513 | "# Опишем граф в виде словаря списков\n", 514 | "graph = {}\n", 515 | "graph['you'] = ['alice','bob','claire']\n", 516 | "graph['bob'] = ['anuj','peggy','you']\n", 517 | "graph['alice'] = ['peggy','you']\n", 518 | "graph['claire'] = ['thom','jonny','you']\n", 519 | "graph['anuj'] = ['bob']\n", 520 | "graph['peggy'] = ['bob','alice']\n", 521 | "graph['thom'] = ['claire']\n", 522 | "graph['jonny'] = ['claire']\n", 523 | "graph" 524 | ] 525 | }, 526 | { 527 | "cell_type": "code", 528 | "execution_count": 106, 529 | "metadata": {}, 530 | "outputs": [ 531 | { 532 | "data": { 533 | "text/plain": [ 534 | "{'claire': False,\n", 535 | " 'thom': False,\n", 536 | " 'alice': False,\n", 537 | " 'you': False,\n", 538 | " 'jonny': False,\n", 539 | " 'anuj': True,\n", 540 | " 'peggy': False,\n", 541 | " 'bob': False}" 542 | ] 543 | }, 544 | "execution_count": 106, 545 | "metadata": {}, 546 | "output_type": "execute_result" 547 | } 548 | ], 549 | "source": [ 550 | "# Словарь с информацией о том является ли человек продавцом\n", 551 | "mango_seller_info = {}\n", 552 | "for name in set(name for name in graph):\n", 553 | " mango_seller_info.setdefault(name,False)\n", 554 | "mango_seller_info['anuj'] = True\n", 555 | "mango_seller_info" 556 | ] 557 | }, 558 | { 559 | "cell_type": "code", 560 | "execution_count": 107, 561 | "metadata": {}, 562 | "outputs": [ 563 | { 564 | "data": { 565 | "image/svg+xml": "\n\n\n\n\n\ntest_graph\n\n\n\nYou\n\nYou\n\n\n\nAlice\n\nAlice\n\n\n\nYou->Alice\n\n\n\n\n\nBob\n\nBob\n\n\n\nYou->Bob\n\n\n\n\n\nClaire\n\nClaire\n\n\n\nYou->Claire\n\n\n\n\n\nAlice->You\n\n\n\n\n\nPeggy\n\nPeggy\n\n\n\nAlice->Peggy\n\n\n\n\n\nBob->You\n\n\n\n\n\nAnuj\n\nAnuj\n\n\n\nBob->Anuj\n\n\n\n\n\nBob->Peggy\n\n\n\n\n\nClaire->You\n\n\n\n\n\nThom\n\nThom\n\n\n\nClaire->Thom\n\n\n\n\n\nJonny\n\nJonny\n\n\n\nClaire->Jonny\n\n\n\n\n\nAnuj->Bob\n\n\n\n\n\nPeggy->Alice\n\n\n\n\n\nPeggy->Bob\n\n\n\n\n\nThom->Claire\n\n\n\n\n\nJonny->Claire\n\n\n\n\n\n", 566 | "text/plain": [ 567 | "" 568 | ] 569 | }, 570 | "execution_count": 107, 571 | "metadata": {}, 572 | "output_type": "execute_result" 573 | } 574 | ], 575 | "source": [ 576 | "# для визуализации графа используем graphviz\n", 577 | "tg = graphviz.Digraph('test_graph')\n", 578 | "for name in graph:\n", 579 | " tg.edges((name.capitalize(), i.capitalize()) for i in graph[name])\n", 580 | "tg" 581 | ] 582 | }, 583 | { 584 | "cell_type": "code", 585 | "execution_count": 110, 586 | "metadata": {}, 587 | "outputs": [], 588 | "source": [ 589 | "# Передаем в качестве аргумента исходный узел\n", 590 | "def BFS(root):\n", 591 | " search_queue = deque() # создаем очередь\n", 592 | " search_queue.append(root)\n", 593 | " parents = {root: None} # для трэйсбэка и прямо по нему и будем проверять посещён узел или нет\n", 594 | " while search_queue: # пока очередь не пуста\n", 595 | " person = search_queue.popleft() # достаем очередной узел из очереди\n", 596 | " if mango_seller_info[person]: # проверяем условие (в нашем случае продавец манго)\n", 597 | " res = []\n", 598 | " current = person\n", 599 | " while parents[current] != None:\n", 600 | " res.append(parents[current])\n", 601 | " current = parents[current]\n", 602 | " print('traceback:',[person] + res, 'length =',len(res))\n", 603 | " return person.capitalize() + ' is mango seller!' # при успехе возвращаем сообщение\n", 604 | " else:\n", 605 | " for elem in graph[person]:\n", 606 | " if elem not in parents: \n", 607 | " search_queue += [elem] # в противном случае добавляем всех соседей этого узла\n", 608 | " parents[elem] = person\n", 609 | " return 'There are no mango sellers in graph' # если просмотрели весь граф и не нашли необходимого, возвращаем сообщение" 610 | ] 611 | }, 612 | { 613 | "cell_type": "code", 614 | "execution_count": 111, 615 | "metadata": {}, 616 | "outputs": [ 617 | { 618 | "name": "stdout", 619 | "output_type": "stream", 620 | "text": [ 621 | "traceback: ['anuj', 'bob', 'you'] length = 2\n" 622 | ] 623 | }, 624 | { 625 | "data": { 626 | "text/plain": [ 627 | "'Anuj is mango seller!'" 628 | ] 629 | }, 630 | "execution_count": 111, 631 | "metadata": {}, 632 | "output_type": "execute_result" 633 | } 634 | ], 635 | "source": [ 636 | "BFS('you')" 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "metadata": {}, 642 | "source": [ 643 | "#### Усложняем" 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": 112, 649 | "metadata": {}, 650 | "outputs": [], 651 | "source": [ 652 | "from typing import List, Tuple, Dict" 653 | ] 654 | }, 655 | { 656 | "cell_type": "code", 657 | "execution_count": 113, 658 | "metadata": {}, 659 | "outputs": [], 660 | "source": [ 661 | "# Попробую реализовать поиск кратчайшего пути на карте в виде квадратной сетки.\n", 662 | "# \"Лабиринт\" будет выглядеть вот так. Это сетка 7х7.\n", 663 | "# S - start, G - goal, # - wall\n", 664 | "'''# # # # # . #\n", 665 | " # . . . . . #\n", 666 | " # . # . # # #\n", 667 | " # . # . . . .\n", 668 | " S . # . # # .\n", 669 | " . . # . # G .\n", 670 | " # # # . # . .'''\n", 671 | "\n", 672 | "# Заведём тип, координата тайла\n", 673 | "GridCoord = Tuple[int,int]\n", 674 | "\n", 675 | "# Создадим класс SquareGrid, который будет описывать прямоугольную сетку из тайлов.\n", 676 | "class SquareGrid():\n", 677 | " def __init__(self, width: int, height: int):\n", 678 | " self.width = width\n", 679 | " self.height = height\n", 680 | " self.walls: List[GridCoord] = [] # координаты стенок\n", 681 | " \n", 682 | " # проверка на нахождение в границах сетки\n", 683 | " def in_bounds(self, id: GridCoord) -> bool: \n", 684 | " (x,y) = id\n", 685 | " return (0 <= x < self.width) and (0 <= y < self.height)\n", 686 | " \n", 687 | " # проверка не является ли координата стенкой\n", 688 | " def passable(self, id: GridCoord) -> bool:\n", 689 | " return (id not in self.walls) \n", 690 | " \n", 691 | " # Вычисление соседей. Незачем запоминать весь список соседей.\n", 692 | " # На ходу проверяем в границах ли ближайшие 4 координаты справа, слева, сверху и снизу\n", 693 | " # И не являются ли они стенами. Возвращаем список координат соседей.\n", 694 | " def neighbours(self, id: GridCoord) -> List[GridCoord]:\n", 695 | " (x,y) = id\n", 696 | " neighbours = [(x+1,y),(x-1,y),(x,y-1),(x,y+1)]\n", 697 | " if (x+y) % 2 == 0: neighbours.reverse()\n", 698 | " result = list(filter(self.in_bounds, neighbours))\n", 699 | " result = list(filter(self.passable, result))\n", 700 | " return result\n", 701 | "\n", 702 | "# Вспомогательная функция, чтобы просто передавать порядковые номера тайлов\n", 703 | "# а не писать полотно из кортежей\n", 704 | "def from_number(number: int, width: int, height: int) -> GridCoord:\n", 705 | " return number%width, number//height\n", 706 | "\n", 707 | "# Функции визуализации одного тайла\n", 708 | "def plot_tile(graph, id, style):\n", 709 | " r = \"\\033[0m . \"\n", 710 | " # Это для визуализации весов рёбер, для последующей реализации алгоритма Дейкстры\n", 711 | " if 'number' in style and id in style['number']: r = \"\\033[0m\" + \" %-2d\" % style['number'][id]\n", 712 | " # Если отработал любой алгоритм и есть список \"родителей\",\n", 713 | " # можно визуализировать откуда пришли в каждый тайл\n", 714 | " if 'point_to' in style and style['point_to'].get(id, None) is not None:\n", 715 | " (x1, y1) = id\n", 716 | " (x2, y2) = style['point_to'][id]\n", 717 | " if x2 == x1 + 1: r = \" > \"\n", 718 | " if x2 == x1 - 1: r = \" < \"\n", 719 | " if y2 == y1 + 1: r = \" v \"\n", 720 | " if y2 == y1 - 1: r = \" ^ \"\n", 721 | " if 'forest' in style and id in style['forest']:\n", 722 | " r = \"\\033[42m \" + '%-2d' % style['number'][id]\n", 723 | " # Если есть путь, то визуализиуем его\n", 724 | " if 'path' in style and id in style['path']: r = \"\\033[0m @ \"\n", 725 | " # Здесь отмечаем стартовую и финишную точки\n", 726 | " if 'start' in style and id == style['start']: r = \"\\033[0m\\33[31m S \"\n", 727 | " if 'goal' in style and id == style['goal']: r = \"\\033[0m\\33[33m G \"\n", 728 | " # И если есть стенки - их тоже визуализируем\n", 729 | " if id in graph.walls: r = \"\\033[0m###\"\n", 730 | " return r\n", 731 | "\n", 732 | "# Функция для визуализации всей сетки \n", 733 | "def plot_grid(graph, **style):\n", 734 | " print(\"___\" * graph.width)\n", 735 | " for y in range(graph.height):\n", 736 | " for x in range(graph.width):\n", 737 | " print(\"%s\" % plot_tile(graph, (x, y), style), end=\"\")\n", 738 | " print()\n", 739 | " print(\"~~~\" * graph.width)\n", 740 | "\n", 741 | "# Сам алгоритм поиска, всё так, же как и в предыдущей реализации\n", 742 | "def breadth_first_search(graph, start: GridCoord, goal: GridCoord):\n", 743 | " queue = deque() # Создаём очередь\n", 744 | " queue += [start] # Добавляем начальный узел\n", 745 | " parents: Dict[GridCoord, List[GridCoord]] = {} # Инициализируем словарь родителей для узлов\n", 746 | " parents[start] = None # Для исходного узла родителей нет\n", 747 | " \n", 748 | " while queue: # Пока очередь не пуста\n", 749 | " current: GridCoord = queue.popleft() # Достаём первый узел из очереди\n", 750 | " \n", 751 | " if current == goal: # Если нашли цель, завершаем алгоритм\n", 752 | " break\n", 753 | " \n", 754 | " for next in graph.neighbours(current): # иначе вытаскиваем всех соседей\n", 755 | " if next not in parents: # если нету в уже пройденных\n", 756 | " queue += [next] # добавляем в очередь\n", 757 | " parents[next] = current # назначаем текущий тайл родителем для каждого из соседей\n", 758 | " \n", 759 | " return parents\n", 760 | "\n", 761 | "# Функция воссоздания пути.\n", 762 | "# Передаём словарь \"родителей\" и стартовые и финишные координаты\n", 763 | "def reconstruct_path(parents: Dict[GridCoord, GridCoord],\n", 764 | " start: GridCoord, goal: GridCoord) -> List[GridCoord]:\n", 765 | " \n", 766 | " current: GridCoord = goal # Начинаем с финиша\n", 767 | " path: List[GridCoord] = [] # Инициализируем путь\n", 768 | " while current != start: # пока текущий узел не является стартовым\n", 769 | " path.append(current) # добавляем сам узел в путь\n", 770 | " current = parents[current] # назначаем \"родительский\" узел текущим\n", 771 | " path.append(start) # в конце добавляем стартовый узел\n", 772 | " path.reverse() # разворачиваем, чтобы путь от начала до конца выводился, а не наоборот\n", 773 | " return path" 774 | ] 775 | }, 776 | { 777 | "cell_type": "code", 778 | "execution_count": 115, 779 | "metadata": {}, 780 | "outputs": [ 781 | { 782 | "name": "stdout", 783 | "output_type": "stream", 784 | "text": [ 785 | "_____________________\n", 786 | "\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m . \u001b[0m###\n", 787 | "\u001b[0m###\u001b[0m . \u001b[0m . \u001b[0m . \u001b[0m . \u001b[0m . \u001b[0m###\n", 788 | "\u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m###\u001b[0m###\n", 789 | "\u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m . \u001b[0m . \u001b[0m . \u001b[0m . \n", 790 | "\u001b[0m\u001b[31m S \u001b[0m . \u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m###\u001b[0m . \n", 791 | "\u001b[0m . \u001b[0m . \u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m\u001b[33m G \u001b[0m . \n", 792 | "\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m . \u001b[0m###\u001b[0m . \u001b[0m . \n", 793 | "~~~~~~~~~~~~~~~~~~~~~\n", 794 | "_____________________\n", 795 | "\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m###\u001b[0m### v \u001b[0m###\n", 796 | "\u001b[0m###\u001b[0m @ \u001b[0m @ \u001b[0m @ < < \u001b[0m###\n", 797 | "\u001b[0m###\u001b[0m @ \u001b[0m###\u001b[0m @ \u001b[0m###\u001b[0m###\u001b[0m###\n", 798 | "\u001b[0m###\u001b[0m @ \u001b[0m###\u001b[0m @ \u001b[0m @ \u001b[0m @ \u001b[0m @ \n", 799 | "\u001b[0m\u001b[31m S \u001b[0m @ \u001b[0m### ^ \u001b[0m###\u001b[0m###\u001b[0m @ \n", 800 | " ^ < \u001b[0m### ^ \u001b[0m###\u001b[0m\u001b[33m G \u001b[0m @ \n", 801 | "\u001b[0m###\u001b[0m###\u001b[0m### ^ \u001b[0m###\u001b[0m . ^ \n", 802 | "~~~~~~~~~~~~~~~~~~~~~\n" 803 | ] 804 | } 805 | ], 806 | "source": [ 807 | "# Проверяем. Создаём сетку 7х7\n", 808 | "mygrid = SquareGrid(7,7)\n", 809 | "# генерим координаты стенок\n", 810 | "mygrid.walls = [from_number(i,7,7) for i in map(lambda x: x-1, [1,2,3,4,5,7,8,14,15,17,19,20,21,22,24,31,33,34,38,40,43,44,45,47])]\n", 811 | "# Словарь с дополнительными параметрами для визуализации\n", 812 | "style = {}\n", 813 | "style['start'] = (0,4) # стартовые координаты\n", 814 | "style['goal'] = (5,5) # финишные координаты\n", 815 | "plot_grid(mygrid, **style)\n", 816 | "\n", 817 | "# Записываем в словарь результаты работы алгоритма и восстановления пути\n", 818 | "style['point_to'] = breadth_first_search(mygrid, (0,4), (5,5))\n", 819 | "style['path'] = reconstruct_path(style['point_to'], style['start'], style['goal'])\n", 820 | "plot_grid(mygrid, **style)" 821 | ] 822 | }, 823 | { 824 | "cell_type": "markdown", 825 | "metadata": {}, 826 | "source": [ 827 | "### 9. Алгоритм Дейкстры.\n", 828 | "- Работает только на направленных ациклических графах. DAG(directed acyclic grph)\n", 829 | "- Рёбра не могут иметь отрицательного веса. Для рёбер с отрицательным весом надо использовать алгоритм Беллмана-Форда\n", 830 | "- Находит маршрут с минимальной стоимостью" 831 | ] 832 | }, 833 | { 834 | "cell_type": "code", 835 | "execution_count": 116, 836 | "metadata": {}, 837 | "outputs": [], 838 | "source": [ 839 | "# Создаем взвешенный граф\n", 840 | "# Значения - веса рёбер\n", 841 | "weighted_graph = {'Книга':{'Пластинка':5, \"Постер\":0},\n", 842 | " 'Пластинка': {'Бас-гитара':15, \"Барабан\":20},\n", 843 | " 'Постер': {'Бас-гитара':30, \"Барабан\":35},\n", 844 | " 'Бас-гитара': {'Пианино':20},\n", 845 | " 'Барабан': {\"Пианино\":10},\n", 846 | " 'Пианино': {}\n", 847 | " }\n", 848 | "\n", 849 | "# Создаём словарь стоимостей для каждого узла\n", 850 | "unk = float('inf')\n", 851 | "costs = {\"Книга\": 0,\n", 852 | " \"Пластинка\": 5,\n", 853 | " \"Постер\": 0,\n", 854 | " \"Бас-гитара\": unk,\n", 855 | " \"Барабан\": unk,\n", 856 | " \"Пианино\": unk\n", 857 | " }\n", 858 | "\n", 859 | "# Создаём словарь родителей\n", 860 | "parents = {'Книга': None,\n", 861 | " 'Пластинка': \"Книга\",\n", 862 | " \"Постер\": \"Книга\",\n", 863 | " \"Бас-гитара\": None,\n", 864 | " \"Барабан\": None,\n", 865 | " \"Пианино\": None}" 866 | ] 867 | }, 868 | { 869 | "cell_type": "code", 870 | "execution_count": 117, 871 | "metadata": {}, 872 | "outputs": [ 873 | { 874 | "data": { 875 | "image/svg+xml": "\n\n\n\n\n\n\n\n\nКнига\n\nКнига\n\n\n\nПластинка\n\nПластинка\n\n\n\nКнига->Пластинка\n\n\n5\n\n\n\nПостер\n\nПостер\n\n\n\nКнига->Постер\n\n\n0\n\n\n\nБас-гитара\n\nБас-гитара\n\n\n\nПластинка->Бас-гитара\n\n\n15\n\n\n\nБарабан\n\nБарабан\n\n\n\nПластинка->Барабан\n\n\n20\n\n\n\nПостер->Бас-гитара\n\n\n30\n\n\n\nПостер->Барабан\n\n\n35\n\n\n\nПианино\n\nПианино\n\n\n\nБас-гитара->Пианино\n\n\n20\n\n\n\nБарабан->Пианино\n\n\n10\n\n\n\n", 876 | "text/plain": [ 877 | "" 878 | ] 879 | }, 880 | "execution_count": 117, 881 | "metadata": {}, 882 | "output_type": "execute_result" 883 | } 884 | ], 885 | "source": [ 886 | "# Визуализируем взвешенный граф\n", 887 | "test_graph = graphviz.Digraph()\n", 888 | "for elem in weighted_graph:\n", 889 | " for child in weighted_graph[elem]:\n", 890 | " test_graph.edge(elem,child, label=str(weighted_graph[elem][child]))\n", 891 | "test_graph" 892 | ] 893 | }, 894 | { 895 | "cell_type": "code", 896 | "execution_count": 118, 897 | "metadata": {}, 898 | "outputs": [ 899 | { 900 | "name": "stdout", 901 | "output_type": "stream", 902 | "text": [ 903 | "Последовательность обменов: Книга->Пластинка->Барабан->Пианино \n", 904 | "Итоговая стоимость: 35\n" 905 | ] 906 | } 907 | ], 908 | "source": [ 909 | "from functools import reduce\n", 910 | "# Сам алгоритм\n", 911 | "def dijkstra_algorithm(graph, costs, parents, start, goal) -> list:\n", 912 | " # Список обработанных узлов\n", 913 | " processed = []\n", 914 | " # Начинаем со стартового узла\n", 915 | " current_node = start\n", 916 | " while current_node is not None: # пока остаются необработанные узлы\n", 917 | " cost = costs[current_node] # Стоимость текущего узла\n", 918 | " neighbours = graph[current_node] # соседи текущего узла\n", 919 | " for n in neighbours.keys():\n", 920 | " new_cost = cost + neighbours[n] # обновляем стоимость каждого соседа\n", 921 | " if costs[n] > new_cost: # если стоимость получилась ниже обновляем стоимость и назначаем нового родителя\n", 922 | " costs[n] = new_cost\n", 923 | " parents[n] = current_node\n", 924 | " processed.append(current_node)\n", 925 | " # Текущим узлом становится узел с минимальной стоимостью\n", 926 | " current_node = find_lowest_cost_node(costs, processed)\n", 927 | " # Возвращаем путь и общую стоимость.\n", 928 | " return path_reconstruction(parents, start, goal), costs[goal]\n", 929 | "\n", 930 | "# Восстановление пути\n", 931 | "def path_reconstruction(parents,start, goal) -> list:\n", 932 | " path = []\n", 933 | " current_node = goal\n", 934 | " while current_node != start:\n", 935 | " path.append(current_node)\n", 936 | " current_node = parents[current_node]\n", 937 | " path.append(current_node)\n", 938 | " path.reverse()\n", 939 | " return path\n", 940 | "\n", 941 | "# Нахождение узла с минимальной стоимостью\n", 942 | "def find_lowest_cost_node(costs, processed):\n", 943 | " lowest_cost = float('inf')\n", 944 | " lowest_cost_node = None\n", 945 | " for node in costs:\n", 946 | " cost = costs[node]\n", 947 | " if (node not in processed) and (cost < lowest_cost):\n", 948 | " lowest_cost = cost\n", 949 | " lowest_cost_node = node\n", 950 | " return lowest_cost_node\n", 951 | "\n", 952 | "\n", 953 | "trace, cost = dijkstra_algorithm(weighted_graph,costs,parents,'Книга','Пианино')\n", 954 | "print('Последовательность обменов:', reduce(lambda x,y: x+'->'+y, trace),'\\nИтоговая стоимость:', cost)\n", 955 | " " 956 | ] 957 | }, 958 | { 959 | "cell_type": "markdown", 960 | "metadata": {}, 961 | "source": [ 962 | "#### Усложняем" 963 | ] 964 | }, 965 | { 966 | "cell_type": "code", 967 | "execution_count": 121, 968 | "metadata": {}, 969 | "outputs": [], 970 | "source": [ 971 | "# Опять же попробуем реализовать алгоритм Дейкстры для сетки.\n", 972 | "# Например это игровая карта с разными стоимостями прохода по разным клеткам.\n", 973 | "\n", 974 | "# Можно не делать отдельную функцию для поиска узла с наименьшей стоимостью.\n", 975 | "# Вместо этого можно воспользоваться реализацией двоичной кучи из модуля heapq\n", 976 | "import heapq\n", 977 | "from typing import TypeVar, Optional\n", 978 | "\n", 979 | "# произвольный тип\n", 980 | "T = TypeVar('T')\n", 981 | "\n", 982 | "# класс сетки с весами.\n", 983 | "# наследник обычной сетки\n", 984 | "# при инициализации добавляем веса, добавляем метод cost возвращающий стоимость достижения узла\n", 985 | "class GridWithWeights(SquareGrid):\n", 986 | " def __init__(self, width: int, height: int):\n", 987 | " super().__init__(width, height)\n", 988 | " self.weights: Dict[GridCoord, float] = {}\n", 989 | " \n", 990 | " def cost(self, to_node: GridCoord) -> float:\n", 991 | " return self.weights.get(to_node, 1) # возвращает 1 если веса нет в словаре.\n", 992 | "\n", 993 | "# Обёртка для очереди с приоритетом, на базе двоичной кучи\n", 994 | "# Заворачиваем push и pop в методы get и put для удобства\n", 995 | "# добавляем метод, возвращающий True если очередь пуста.\n", 996 | "class PriorityQueue:\n", 997 | " def __init__(self):\n", 998 | " self.elements: List[Tuple[float, T]] = []\n", 999 | " \n", 1000 | " def empty(self) -> bool:\n", 1001 | " return not self.elements\n", 1002 | " \n", 1003 | " def put(self, item: T, priority: float):\n", 1004 | " heapq.heappush(self.elements, (priority, item))\n", 1005 | " \n", 1006 | " def get(self) -> T:\n", 1007 | " return heapq.heappop(self.elements)[1]\n", 1008 | "\n", 1009 | "# Сам алгоритм\n", 1010 | "# Передаем в качестве атрибутов граф, координаты старта и финиша\n", 1011 | "def dijkstra_search(graph: GridWithWeights, start: GridCoord, goal: GridCoord):\n", 1012 | " queue = PriorityQueue() # Инициализируем очередь.\n", 1013 | " queue.put(start, 0) # добавляем в очередь стартовый \"узел\"\n", 1014 | " parents: Dict[GridCoord, Optional[GridCoord]] = {} # инициализируем словарь \"родителей\"\n", 1015 | " costs: Dict[GridCoord, float] = {} # инициализируем словарь со стоимостями для каждого узла\n", 1016 | " parents[start] = None # у начального узла родителей нет\n", 1017 | " costs[start] = 0 # стоимость его достижения - нулевая\n", 1018 | " \n", 1019 | " while not queue.empty(): # пока очередь не пуста\n", 1020 | " current: GridCoord = queue.get() # получаем узел с наименьшим приоритетом\n", 1021 | " \n", 1022 | " if current == goal: # если это финишный узел - алгоритм заканчивает работу\n", 1023 | " break\n", 1024 | " \n", 1025 | " for next in graph.neighbours(current): # если нет, то для каждого из соседей\n", 1026 | " new_cost = costs[current] + graph.cost(next) # рассчитываем стоимость его достижения через текущий узел\n", 1027 | " if next not in costs or new_cost < costs[next]: # если новая стоимость меньше текущей\n", 1028 | " costs[next] = new_cost # обновляем стоимость\n", 1029 | " priority = new_cost # записываем её как приоритет\n", 1030 | " queue.put(next, priority) # добавляем узел в очередь, вместе с приоритетом\n", 1031 | " parents[next] = current # обновляем \"родительский\" узел для этого соседа\n", 1032 | " \n", 1033 | " return parents, costs # возвращаем список родителей и список стоимостей" 1034 | ] 1035 | }, 1036 | { 1037 | "cell_type": "code", 1038 | "execution_count": 123, 1039 | "metadata": {}, 1040 | "outputs": [ 1041 | { 1042 | "name": "stdout", 1043 | "output_type": "stream", 1044 | "text": [ 1045 | "______________________________\n", 1046 | "\u001b[0m 5 \u001b[0m 4 \u001b[0m 5 \u001b[0m 6 \u001b[0m 7 \u001b[0m 8 \u001b[0m 9 \u001b[0m 10\u001b[0m 11\u001b[0m 12\n", 1047 | "\u001b[0m 4 \u001b[0m 3 \u001b[0m 4 \u001b[0m 5 \u001b[42m 26\u001b[42m 31\u001b[0m 10\u001b[0m 11\u001b[0m 12\u001b[0m 13\n", 1048 | "\u001b[0m 3 \u001b[0m 2 \u001b[0m 3 \u001b[0m 4 \u001b[42m 29\u001b[42m 18\u001b[42m 22\u001b[0m 12\u001b[0m 13\u001b[0m 14\n", 1049 | "\u001b[0m 2 \u001b[0m 1 \u001b[0m 2 \u001b[0m 3 \u001b[42m 18\u001b[42m 16\u001b[42m 48\u001b[42m 17\u001b[0m 14\u001b[0m 15\n", 1050 | "\u001b[0m 1 \u001b[0m 1 \u001b[0m 1 \u001b[42m 18\u001b[42m 34\u001b[42m 46\u001b[42m 21\u001b[42m 20\u001b[0m 15\u001b[0m 16\n", 1051 | "\u001b[0m 2 \u001b[0m 1 \u001b[0m 2 \u001b[42m 7 \u001b[42m 29\u001b[42m 47\u001b[42m 22\u001b[42m 21\u001b[0m 6 \u001b[0m 17\n", 1052 | "\u001b[0m 3 \u001b[0m 2 \u001b[0m 3 \u001b[0m 4 \u001b[42m 9 \u001b[42m 34\u001b[42m 29\u001b[0m 36\u001b[0m 17\u001b[0m 18\n", 1053 | "\u001b[0m 4 \u001b[0m###\u001b[0m###\u001b[0m###\u001b[42m 14\u001b[42m 29\u001b[42m 18\u001b[0m 15\u001b[0m 16\u001b[0m 17\n", 1054 | "\u001b[0m 5 \u001b[0m###\u001b[0m###\u001b[0m###\u001b[42m 15\u001b[42m 16\u001b[0m 13\u001b[0m 14\u001b[0m 15\u001b[0m 16\n", 1055 | "\u001b[0m 6 \u001b[0m 7 \u001b[0m 8 \u001b[0m 9 \u001b[0m 10\u001b[0m 11\u001b[0m 12\u001b[0m 13\u001b[0m 14\u001b[0m 15\n", 1056 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", 1057 | "______________________________\n", 1058 | "\u001b[0m 5 \u001b[0m @ \u001b[0m @ \u001b[0m @ \u001b[0m @ \u001b[0m @ \u001b[0m @ \u001b[0m 10\u001b[0m 11\u001b[0m 12\n", 1059 | "\u001b[0m 4 \u001b[0m @ \u001b[0m 4 \u001b[0m 5 \u001b[42m 26\u001b[42m 31\u001b[0m @ \u001b[0m @ \u001b[0m 12\u001b[0m 13\n", 1060 | "\u001b[0m 3 \u001b[0m @ \u001b[0m 3 \u001b[0m 4 \u001b[42m 29\u001b[42m 18\u001b[42m 22\u001b[0m @ \u001b[0m @ \u001b[0m 14\n", 1061 | "\u001b[0m 2 \u001b[0m @ \u001b[0m 2 \u001b[0m 3 \u001b[42m 18\u001b[42m 16\u001b[42m 48\u001b[42m 17\u001b[0m @ \u001b[0m 15\n", 1062 | "\u001b[0m 1 \u001b[0m\u001b[31m S \u001b[0m 1 \u001b[42m 18\u001b[42m 34\u001b[42m 46\u001b[42m 21\u001b[42m 20\u001b[0m @ \u001b[0m 16\n", 1063 | "\u001b[0m 2 \u001b[0m 1 \u001b[0m 2 \u001b[42m 7 \u001b[42m 29\u001b[42m 47\u001b[42m 22\u001b[42m 21\u001b[0m\u001b[33m G \u001b[0m 17\n", 1064 | "\u001b[0m 3 \u001b[0m 2 \u001b[0m 3 \u001b[0m 4 \u001b[42m 9 \u001b[42m 34\u001b[42m 29\u001b[0m 36\u001b[0m 17\u001b[0m 18\n", 1065 | "\u001b[0m 4 \u001b[0m###\u001b[0m###\u001b[0m###\u001b[42m 14\u001b[42m 29\u001b[42m 18\u001b[0m 15\u001b[0m 16\u001b[0m 17\n", 1066 | "\u001b[0m 5 \u001b[0m###\u001b[0m###\u001b[0m###\u001b[42m 15\u001b[42m 16\u001b[0m 13\u001b[0m 14\u001b[0m 15\u001b[0m 16\n", 1067 | "\u001b[0m 6 \u001b[0m 7 \u001b[0m 8 \u001b[0m 9 \u001b[0m 10\u001b[0m 11\u001b[0m 12\u001b[0m 13\u001b[0m 14\u001b[0m 15\n", 1068 | "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", 1069 | "Минимальная стоимость пути: 126\n" 1070 | ] 1071 | } 1072 | ], 1073 | "source": [ 1074 | "# Создадим сетку 10х10. Карту с \"горой\" и \"лесом\".\n", 1075 | "# Гора - стенки. Лес - тайлы с большими весами в центре.\n", 1076 | "game_map = GridWithWeights(10,10)\n", 1077 | "game_map.weights = dict(zip([(i,j) for j in range(10) for i in range(10)], \n", 1078 | " [5,4,5,6,7,8,9,10,11,12,\n", 1079 | " 4,3,4,5,26,31,10,11,12,13,\n", 1080 | " 3,2,3,4,29,18,22,12,13,14,\n", 1081 | " 2,1,2,3,18,16,48,17,14,15,\n", 1082 | " 1,1,1,18,34,46,21,20,15,16,\n", 1083 | " 2,1,2,7,29,47,22,21,6,17,\n", 1084 | " 3,2,3,4,9,34,29,36,17,18,\n", 1085 | " 4,1,1,1,14,29,18,15,16,17,\n", 1086 | " 5,1,1,1,15,16,13,14,15,16,\n", 1087 | " 6,7,8,9,10,11,12,13,14,15]))\n", 1088 | "game_map.walls = [from_number(i,10,10) for i in [71,72,73,81,82,83]]\n", 1089 | "forest = [from_number(i,10,10) for i in [14,15,24,25,26,34,35,36,37,43,44,45,46,47,53,54,55,56,57,64,65,66,74,75,76,84,85]]\n", 1090 | "start = (1,4)\n", 1091 | "goal = (8,5)\n", 1092 | "plot_grid(game_map, number=game_map.weights, forest=forest)\n", 1093 | "\n", 1094 | "p,c = dijkstra_search(game_map,start,goal)\n", 1095 | "\n", 1096 | "plot_grid(game_map, number=game_map.weights, start=start, goal=goal, forest=forest, path = reconstruct_path(p,start,goal))\n", 1097 | "\n", 1098 | "print('Минимальная стоимость пути:', c[goal])" 1099 | ] 1100 | }, 1101 | { 1102 | "cell_type": "markdown", 1103 | "metadata": {}, 1104 | "source": [ 1105 | "## Жадные алгоритмы\n", 1106 | "- Основная идея: на каждом шаге выбирается локально-оптимальное решение. \n", 1107 | "- По итогу получается глобальное решение близкое к оптимуму. Но вовсе не обязательно оптимальное.\n", 1108 | "- Позволяют решать NP-полные задачи с приемлемой точностью за адекватное время." 1109 | ] 1110 | }, 1111 | { 1112 | "cell_type": "markdown", 1113 | "metadata": {}, 1114 | "source": [ 1115 | "### 10. Покрытие множества\n", 1116 | "- Вводные: есть 5 радиостанций, каждая покрывает определённое количество штатов. \n", 1117 | "- Есть 8 штатов.\n", 1118 | "- Необходимо найти минимальный набор радиостанций, который покрывает все требуемые штаты." 1119 | ] 1120 | }, 1121 | { 1122 | "cell_type": "code", 1123 | "execution_count": 124, 1124 | "metadata": {}, 1125 | "outputs": [ 1126 | { 1127 | "name": "stdout", 1128 | "output_type": "stream", 1129 | "text": [ 1130 | "{'kthree', 'kone', 'ktwo', 'kfive'}\n" 1131 | ] 1132 | } 1133 | ], 1134 | "source": [ 1135 | "# Воспользуемся множествами\n", 1136 | "# Список штатов, которые необходимо охватить\n", 1137 | "states_needed = {'mt','wa','or','id','nv','ut','ca','az'}\n", 1138 | "\n", 1139 | "# Станции и покрытие каждой из них, в виде словаря множеств\n", 1140 | "stations = {\n", 1141 | " 'kone': {'id','nv','ut'},\n", 1142 | " 'ktwo': {'wa','id','mt'},\n", 1143 | " 'kthree': {'or','nv','ca'},\n", 1144 | " 'kfour': {'nv','ut'},\n", 1145 | " 'kfive': {'ca','az'}\n", 1146 | " }\n", 1147 | "\n", 1148 | "# Итоговый список станций.\n", 1149 | "final_stations = set()\n", 1150 | "\n", 1151 | "\n", 1152 | "# Сам алгоритм.\n", 1153 | "while states_needed: # пока список штатов, которые надо охватить не пуст\n", 1154 | " # выбираем станцию, которая покрывает наибольшее количество ещё не покрытых штатов.\n", 1155 | " best_station = None # Лучшая по покрытию станция\n", 1156 | " states_covered = set() # Штаты которые охватывает станция, из тех, что ещё не охвачены.\n", 1157 | " for station, states in stations.items(): # Перебираем все станции из списка\n", 1158 | " covered = states_needed & states # Пересечение двух множеств. Между оставшимися неохваченными штатами и штатами, которые охватывает текущая станция.\n", 1159 | " if len(covered) > len(states_covered): # Если станция покрывает больше штатов, чем текущая, из тех что ещё не охвачены\n", 1160 | " best_station = station # Назначаем новую лучшую станцию\n", 1161 | " states_covered = covered # Записываем охватываемые штаты\n", 1162 | " states_needed -= states_covered # Убираем охваченные штаты из списка необходимых\n", 1163 | " final_stations.add(best_station) # Добавляем лучшую станцию к итоговому списку\n", 1164 | " # И алгоритм продолжает работать\n", 1165 | " # Можно ещё удалять из словаря уже отобранные станции, \n", 1166 | " # чтобы каждый раз не проверять весь словарь.\n", 1167 | " del stations[best_station]\n", 1168 | "print(final_stations)" 1169 | ] 1170 | }, 1171 | { 1172 | "cell_type": "markdown", 1173 | "metadata": {}, 1174 | "source": [ 1175 | "### 11. Динамическое программирование\n", 1176 | "Основные идеи:\n", 1177 | "- Как и в жадных алгоритмах задача разбивается на подзадачи. Но динамическое программирование применимо к задачам с оптимальной подструктурой. Когда есть набор перекрывающихся подзадач, каждая из которых чуть проще итоговой.\n", 1178 | "- Решаются подзадачи, их решения объединяются для решения глобальной задачи. \n", 1179 | "- Два варианта. Либо прямой проход с мемоизацией(запоминанием) результатов решения подзадач. Либо обратный проход, когда задача рекурсивно разбивается на подзадачи и они решаются.\n", 1180 | "- Важный момент. Если в жадных алгоритмах ищется оптимальное решение только на текущем шаге, то в динамическом программировании учитывается вся цепочка от текущего шага и до конца. И оценивается вся совокупность таких цепочек и уже из них выбирается оптимальный вариант.\n", 1181 | " - По-сути всё сводится к уравнению Беллмана. Если брать дискретные состояния. Для непрерывных - это уравнение Гамильтона-Якоби-Беллмана.\n", 1182 | " - $V(x) = \\max\\limits_{a\\in\\Gamma(x)} \\{F(x,a)+\\beta V(T(x,a))\\}$ - уравнение Беллмана\n", 1183 | " - $x$ - состояние, $a$ - действие, $\\Gamma(x)$ - пространство возможных действий для текущего состояния, $T(x,a)$ - функция нового состояния, зависит от текущего состояния и выбранного действия\n", 1184 | " - $F(x,a)$ - функция вознаграждения (payoff). $\\beta$ - коэффициент дисконтирования. $V(x)$ - Функция стоимости(value function). \n", 1185 | " - Решением как раз является нахождение такой функции, которая максимизирует суммарное дисконтированное вознаграждение.\n", 1186 | " - В решении задач о размене и о рюкзаке мы суммы от 0 до N и вместимость рюкзака от 0 до N считаем состояниями. А номиналы монет и веса предметов - соответственно действиями. Функции вознаграждения уже определены. Тогда как в том же RL(например Q-learning) нам необходимо искать всё это, инициализируя таблицу случайными числами." 1187 | ] 1188 | }, 1189 | { 1190 | "cell_type": "markdown", 1191 | "metadata": {}, 1192 | "source": [ 1193 | "11.а. Самый простой пример.\n", 1194 | "- Числа Фибоначчи.\n", 1195 | "- Часто ещё формулируется как задача о прыжках кузнечика. Есть некий отрезок длины n. Кузнечик может прыгать на 1 и на 2 вперёд. Сколько есть способов добраться до точки n.\n", 1196 | "- Скорость выполнения конечно гораздо быстрее, чем у чистой рекурсии, как минимум потому, что не не нужно вычислять fib для тех значений, которые уже были получены.\n", 1197 | "- Но это дополнительные затраты памяти." 1198 | ] 1199 | }, 1200 | { 1201 | "cell_type": "code", 1202 | "execution_count": 125, 1203 | "metadata": {}, 1204 | "outputs": [ 1205 | { 1206 | "name": "stdout", 1207 | "output_type": "stream", 1208 | "text": [ 1209 | "0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, " 1210 | ] 1211 | } 1212 | ], 1213 | "source": [ 1214 | "# Вариант 1. Рекурсия с кэшированием.\n", 1215 | "def fib_cache(n:int) -> int:\n", 1216 | " cache = [0 for _ in range(n+1)] # создаём список под кэш\n", 1217 | " def _fib_cache(n: int) -> int:\n", 1218 | " if n <= 1: \n", 1219 | " return n\n", 1220 | " if cache[n] == 0: # Если значения нет в кэше, заполняем значение рекурсивно вызывая функцию для n-1 и n-2\n", 1221 | " cache[n] = _fib_cache(n-1)+_fib_cache(n-2)\n", 1222 | " return cache[n]\n", 1223 | " return _fib_cache(n)\n", 1224 | "\n", 1225 | "for i in range(50):\n", 1226 | " print(fib_cache(i), end=', ')" 1227 | ] 1228 | }, 1229 | { 1230 | "cell_type": "code", 1231 | "execution_count": 126, 1232 | "metadata": {}, 1233 | "outputs": [ 1234 | { 1235 | "name": "stdout", 1236 | "output_type": "stream", 1237 | "text": [ 1238 | "0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, " 1239 | ] 1240 | } 1241 | ], 1242 | "source": [ 1243 | "# Вариант 2. Без рекурсии.\n", 1244 | "def fib_forward(n):\n", 1245 | " f = [0 for _ in range(max(2,n+1))] # Создаём массив размерам минимум 2, максимум n+1\n", 1246 | " # Заполняем первые два значения\n", 1247 | " f[0] = 0 \n", 1248 | " f[1] = 1\n", 1249 | " for i in range(2,n+1): # Проходимся циклом, заполняя массив\n", 1250 | " f[i] = f[i-1]+f[i-2] # Каждый элемент это сумма двух предыдущих\n", 1251 | " return f[n]\n", 1252 | "\n", 1253 | "for i in range(50):\n", 1254 | " print(fib_forward(i), end=', ')" 1255 | ] 1256 | }, 1257 | { 1258 | "cell_type": "code", 1259 | "execution_count": 127, 1260 | "metadata": {}, 1261 | "outputs": [ 1262 | { 1263 | "name": "stdout", 1264 | "output_type": "stream", 1265 | "text": [ 1266 | "0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, " 1267 | ] 1268 | } 1269 | ], 1270 | "source": [ 1271 | "# То же, используя только 2 переменных, для хранения результатов.\n", 1272 | "def fib_new(n: int) -> int:\n", 1273 | " f = [0,1]\n", 1274 | " for i in range(2,n+1):\n", 1275 | " f[0],f[1] = f[1],f[0]+f[1]\n", 1276 | " return f[0] if n==0 else f[1]\n", 1277 | "\n", 1278 | "for i in range(50):\n", 1279 | " print(fib_new(i), end=', ')" 1280 | ] 1281 | }, 1282 | { 1283 | "cell_type": "markdown", 1284 | "metadata": {}, 1285 | "source": [ 1286 | "11.б. Задача о сдаче(размене).\n", 1287 | "- Есть набор монет разных номиналов (например 1,3,5,10 копеек).\n", 1288 | "- Нужно найти минимальное количество монет, которыми можно выдать сдачу размера N.\n", 1289 | "- Чем хороша задача в качестве примера - жадные алгоритмы на ней дают неоптимальное решение. \n", 1290 | "- Пример: У нас есть монеты номиналом 10, 9 и 1 копейка. Нужно выдать сдачу 18 копеек. Жадный способ выберет 10 копеек и 8 раз по 1 копейке. Всего 9 монет. Тогда как оптимальное решение в данном случае 2 монеты по 9 копеек." 1291 | ] 1292 | }, 1293 | { 1294 | "cell_type": "code", 1295 | "execution_count": 128, 1296 | "metadata": {}, 1297 | "outputs": [ 1298 | { 1299 | "name": "stdout", 1300 | "output_type": "stream", 1301 | "text": [ 1302 | "Amount: 18, coins: [1,9,10], Result: 2\n", 1303 | "Amount: 18, coins: [1,3,5], Result: 4\n" 1304 | ] 1305 | } 1306 | ], 1307 | "source": [ 1308 | "# Сам алгоритм\n", 1309 | "def min_change(n: int, coins: tuple) -> int:\n", 1310 | " F=[0,1] # Задаём первые два результата. Для 0 - 0 монет, для 1 - 1 монета в 1 копейку.\n", 1311 | " for i in range(2,n+1): # Цикл от 2 до необходимой суммы.\n", 1312 | " # Рекуррентная функция, 1 + минимум от всех возможных вариантов. Если текущая сумма меньше номинала монеты - мы не можем её учитывать. \n", 1313 | " # Очевидно мы не можем выдать сдачу , к примеру, 4 используя монету номиналом 5.\n", 1314 | " F+=[1+min([F[i-coin] for coin in coins if coin<=i])]\n", 1315 | " return F[n]\n", 1316 | "\n", 1317 | "\n", 1318 | "print('Amount: 18, coins: [1,9,10], Result:', min_change(18,(1,9,10)))\n", 1319 | "print('Amount: 18, coins: [1,3,5], Result:',min_change(18,(1,3,5)))" 1320 | ] 1321 | }, 1322 | { 1323 | "cell_type": "code", 1324 | "execution_count": 129, 1325 | "metadata": {}, 1326 | "outputs": [ 1327 | { 1328 | "name": "stdout", 1329 | "output_type": "stream", 1330 | "text": [ 1331 | "Amount: 18\n", 1332 | "Coins: (1, 3, 5)\n", 1333 | "Matrix:\n", 1334 | "[0 0 0]\n", 1335 | "[1 0 0]\n", 1336 | "[2 0 0]\n", 1337 | "[3 1 0]\n", 1338 | "[2 2 0]\n", 1339 | "[3 3 1]\n", 1340 | "[2 2 2]\n", 1341 | "[3 3 3]\n", 1342 | "[4 2 2]\n", 1343 | "[3 3 3]\n", 1344 | "[4 4 2]\n", 1345 | "[3 3 3]\n", 1346 | "[4 4 4]\n", 1347 | "[5 3 3]\n", 1348 | "[4 4 4]\n", 1349 | "[5 5 3]\n", 1350 | "[4 4 4]\n", 1351 | "[5 5 5]\n", 1352 | "[6 4 4]\n", 1353 | "Result:\n", 1354 | "[3, 5, 5, 5]\n" 1355 | ] 1356 | } 1357 | ], 1358 | "source": [ 1359 | "# То же самое , но теперь сохраняем в виде таблицы все результаты.\n", 1360 | "# Для того, чтобы потом можно было не только количество, но и набор монет для размена выдать.\n", 1361 | "import numpy as np\n", 1362 | "\n", 1363 | "def min_change_advanced(n: int, coins: tuple) -> list:\n", 1364 | " F = np.full((N+1,len(coins)),0) # Создаём таблицу-болванку\n", 1365 | " F[1,0] = 1 # Заполняем первый результат\n", 1366 | " # Это блок, в котором таблица заполняется последовательно.\n", 1367 | " # Проходимся двумя циклами по всем ячейкам (для всех сумм от 2 до необходимой, для каждого из номиналов монет)\n", 1368 | " for i in range(2,N+1):\n", 1369 | " for j in range(len(coins)):\n", 1370 | " if i >= coins[j]: # Если номинал меньше или равен сумме\n", 1371 | " # Тут такая запутанная конструкция с двумя проверками получилась потому, \n", 1372 | " # что нужно выбирать минимум именно из ненулевых значений.\n", 1373 | " # Если же все нули - то в результате будет единица.\n", 1374 | " F[i,j] = 1 +(np.min(F[i-coins[j],F[i-coins[j]]>0]) if any(F[i-coins[j]]) else 0)\n", 1375 | " else: # Если же номинал больше суммы, то мы этой монетой вообще воспользоваться не можем.\n", 1376 | " F[i,j] = 0\n", 1377 | " \n", 1378 | " # А теперь по этой таблице список монет составляем.\n", 1379 | " res = []\n", 1380 | " i = N # Начинаем с последней строки матрицы (исходная сумма)\n", 1381 | " while i > 0:\n", 1382 | " # получаем индекс наименьшего элемента строки\n", 1383 | " # если есть несколько равных эелементов - выбираем первый из них.\n", 1384 | " idx = np.where(F[i] == min(F[i,F[i]>0]))[0][0] \n", 1385 | " res.append(coins[idx]) # добавляем к результату\n", 1386 | " i -= coins[idx] # уменьшаем сумму(в нашем случае номер строки матрицы) на номинал монеты.\n", 1387 | " \n", 1388 | " \n", 1389 | " return F, res\n", 1390 | "\n", 1391 | "coins = (1,3,5)\n", 1392 | "N = 18\n", 1393 | "\n", 1394 | "mat, lst = min_change_advanced(N,coins)\n", 1395 | "print(f'Amount: {N}')\n", 1396 | "print(f'Coins: {coins}')\n", 1397 | "print('Matrix:',*mat,sep='\\n')\n", 1398 | "print(f'Result:\\n{lst}')" 1399 | ] 1400 | }, 1401 | { 1402 | "cell_type": "markdown", 1403 | "metadata": {}, 1404 | "source": [ 1405 | "11.в. Задача о рюкзаке\n", 1406 | "- Задача о сдаче(размене монет) это тоже одна из формулировок задачи о рюкзаке\n", 1407 | "- Разных типов задачи много, с разными условиями и ограничениями\n", 1408 | "- В самом простом виде задача следующая: Есть рюкзак вместимостью $W$, есть $n$ предметов с весами $\\{w_0, w_1, ..., w_n\\}$ и стоимостями $\\{v_0, v_1, ..., v_n\\}$. Необходимо определить какой набор предметов, помещающийся в рюкзак, обеспечит максимальную стоимость.\n", 1409 | "- Принцип решения ровно такой же. Решаем задачу последовательно для рюкзаков меньших ёмкостей/размеров при ограниченном наборе предметов. Комбинируем результаты для получения финального." 1410 | ] 1411 | }, 1412 | { 1413 | "cell_type": "code", 1414 | "execution_count": 140, 1415 | "metadata": {}, 1416 | "outputs": [], 1417 | "source": [ 1418 | "items = ['guitar','tape','laptop', 'iphone']\n", 1419 | "weights = [0,1,4,3,1]\n", 1420 | "values = [0,1500,3000,2000,2000]\n", 1421 | "W = 4" 1422 | ] 1423 | }, 1424 | { 1425 | "cell_type": "code", 1426 | "execution_count": 199, 1427 | "metadata": {}, 1428 | "outputs": [], 1429 | "source": [ 1430 | "def knapsack_0_1(W: int, weights: list, values: list, items: list):\n", 1431 | " m = np.zeros((len(items)+1,W+1)) # Инициализируем таблицу нулями\n", 1432 | " for i in range(1,len(items)+1): # цикл от 1 до количества предметов (по строкам)\n", 1433 | " for j in range(1,W+1): # цикл от 1 до вместимости рюкзака (по столбцам)\n", 1434 | " if weights[i] > j: # если вес предмета, больше чем вместимость рюкзака\n", 1435 | " m[i,j] = m[i-1,j] # то берём предыдущий предмет (из предыдущей строки, то есть от меньшего набора предметов)\n", 1436 | " else:\n", 1437 | " # Иначе выбираем максимум между уже найденным значением для меньшего набора предметов (предыдущая строка)\n", 1438 | " # и суммой стоимости текущего предмета и уже найденной стоимостью для меньшего набора для подрюкзака вместимости (i-вес текущего предмета)\n", 1439 | " \n", 1440 | " # Если проще сформулировать, мы выбираем сначала можем ли мы вообще взять предмет, и если можем, то будет ли выгоднее взять его\n", 1441 | " # или оставить предыдущее решение(для меньшего подрюкзака)\n", 1442 | " m[i,j] = max(m[i-1,j],m[i-1,j-weights[i]]+values[i])\n", 1443 | " \n", 1444 | " # Бэктрэкинг по уже заполненной таблице.\n", 1445 | " # Чтобы список предметов вывести.\n", 1446 | " # Начинаем с нижней правой ячейки\n", 1447 | " j = W\n", 1448 | " i = len(items)-1\n", 1449 | " res_items = []\n", 1450 | " while m[i,j] > 0:\n", 1451 | " if m[i,j] == m[i-1,j]: # Если в предыдущей строке такое же значение, значит предмет не брали.\n", 1452 | " i -= 1\n", 1453 | " else:\n", 1454 | " res_items.append(items[j-1]) # Добавляем предмет, вычитаем из индекса его вес, переходим к новой ячейке.\n", 1455 | " i -= 1\n", 1456 | " j -= weights[j]\n", 1457 | " \n", 1458 | " \n", 1459 | " return m,m[-1,-1], res_items" 1460 | ] 1461 | }, 1462 | { 1463 | "cell_type": "code", 1464 | "execution_count": 200, 1465 | "metadata": {}, 1466 | "outputs": [ 1467 | { 1468 | "name": "stdout", 1469 | "output_type": "stream", 1470 | "text": [ 1471 | "|| Item || Weight || Value ||\n", 1472 | "-------------------------------\n", 1473 | "|| guitar || 1 || 1500 ||\n", 1474 | "|| tape || 4 || 3000 ||\n", 1475 | "|| laptop || 3 || 2000 ||\n", 1476 | "|| iphone || 1 || 2000 ||\n", 1477 | "\n", 1478 | "Matrix\n", 1479 | " [[ 0. 0. 0. 0. 0.]\n", 1480 | " [ 0. 1500. 1500. 1500. 1500.]\n", 1481 | " [ 0. 1500. 1500. 1500. 3000.]\n", 1482 | " [ 0. 1500. 1500. 2000. 3500.]\n", 1483 | " [ 0. 2000. 3500. 3500. 4000.]]\n", 1484 | "\n", 1485 | "Max_value: 4000.0\n", 1486 | "Items: ['iphone', 'laptop']\n" 1487 | ] 1488 | } 1489 | ], 1490 | "source": [ 1491 | "print('|| Item || Weight || Value ||','-'*31, sep='\\n')\n", 1492 | "for i in range(len(items)):\n", 1493 | " print(f'|| {items[i]:<6} || {weights[i+1]:>6} || {values[i+1]:>5} ||')\n", 1494 | "m,res,track = knapsack_0_1(W,weights,values,items)\n", 1495 | "print('\\nMatrix\\n', m)\n", 1496 | "print('\\nMax_value:', res)\n", 1497 | "print('Items:', track)" 1498 | ] 1499 | }, 1500 | { 1501 | "cell_type": "markdown", 1502 | "metadata": {}, 1503 | "source": [ 1504 | "### 12. Алгоритм К-ближайших соседей.(KNN, K-nearest neighbours)\n", 1505 | "- Можно использовать для классификации(путём голосования) и для регрессии(путём усреднения)\n", 1506 | "- Вероятно самый простой алгоритм.\n", 1507 | "- В самом простом варианте - сильно неэффективный. Но при этом очень прост в реализации.\n", 1508 | "- Коротко смысл: \n", 1509 | "
есть обучающая выборка(train) в ней N объектов с признаками(features), для каждого объекта есть целевая переменная (target).\n", 1510 | "
Есть тестовая выборка из M объектов, у которых есть только признаки. \n", 1511 | "
Считаем расстояние (метрики можно использовать разные) от объекта из тестовой выборки до каждого объекта из обучающей.\n", 1512 | "
Выбираем K (параметр заадём сами) ближайших по метрике объектов. Смотрим на их таргеты. \n", 1513 | "
В случае классификации выбираем наиболее частый таргет среди этих K. В случае регрессии можем усреднить значение этих таргетов.\n", 1514 | "- Для классификации можно принять важность каждого из К таргетов одинаковой, либо задать им веса, в зависимости от дистанции от тестового объекта.\n", 1515 | "- При подходе \"в лоб\" нужно считать N*M метрик. Есть более эффективные варианты, основанные на деревьях: \n", 1516 | "
BallTree и KD-Tree (Дерево шаров и к-мерное дерево соответственно).\n", 1517 | "
Их реализации сложнее, лучше расписывать отдельно." 1518 | ] 1519 | }, 1520 | { 1521 | "cell_type": "code", 1522 | "execution_count": 233, 1523 | "metadata": {}, 1524 | "outputs": [], 1525 | "source": [ 1526 | "# Импортируем все нужные либы\n", 1527 | "import numpy as np\n", 1528 | "import pandas as pd\n", 1529 | "import matplotlib.pyplot as plt\n", 1530 | "from sklearn.neighbors import KNeighborsClassifier\n", 1531 | "from sklearn.metrics import accuracy_score\n", 1532 | "from sklearn import datasets" 1533 | ] 1534 | }, 1535 | { 1536 | "cell_type": "code", 1537 | "execution_count": 248, 1538 | "metadata": {}, 1539 | "outputs": [ 1540 | { 1541 | "name": "stdout", 1542 | "output_type": "stream", 1543 | "text": [ 1544 | "(0, 'setosa') (1, 'versicolor') (2, 'virginica')\n" 1545 | ] 1546 | }, 1547 | { 1548 | "data": { 1549 | "text/html": [ 1550 | "
\n", 1551 | "\n", 1564 | "\n", 1565 | " \n", 1566 | " \n", 1567 | " \n", 1568 | " \n", 1569 | " \n", 1570 | " \n", 1571 | " \n", 1572 | " \n", 1573 | " \n", 1574 | " \n", 1575 | " \n", 1576 | " \n", 1577 | " \n", 1578 | " \n", 1579 | " \n", 1580 | " \n", 1581 | " \n", 1582 | " \n", 1583 | " \n", 1584 | " \n", 1585 | " \n", 1586 | " \n", 1587 | " \n", 1588 | " \n", 1589 | " \n", 1590 | " \n", 1591 | " \n", 1592 | " \n", 1593 | " \n", 1594 | " \n", 1595 | " \n", 1596 | " \n", 1597 | " \n", 1598 | " \n", 1599 | " \n", 1600 | " \n", 1601 | " \n", 1602 | " \n", 1603 | " \n", 1604 | " \n", 1605 | " \n", 1606 | " \n", 1607 | " \n", 1608 | " \n", 1609 | " \n", 1610 | " \n", 1611 | " \n", 1612 | " \n", 1613 | " \n", 1614 | " \n", 1615 | " \n", 1616 | " \n", 1617 | "
sepal length (cm)sepal width (cm)petal length (cm)petal width (cm)class
495.03.31.40.20
507.03.24.71.41
224.63.61.00.20
776.73.05.01.71
1106.53.25.12.02
\n", 1618 | "
" 1619 | ], 1620 | "text/plain": [ 1621 | " sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) \\\n", 1622 | "49 5.0 3.3 1.4 0.2 \n", 1623 | "50 7.0 3.2 4.7 1.4 \n", 1624 | "22 4.6 3.6 1.0 0.2 \n", 1625 | "77 6.7 3.0 5.0 1.7 \n", 1626 | "110 6.5 3.2 5.1 2.0 \n", 1627 | "\n", 1628 | " class \n", 1629 | "49 0 \n", 1630 | "50 1 \n", 1631 | "22 0 \n", 1632 | "77 1 \n", 1633 | "110 2 " 1634 | ] 1635 | }, 1636 | "execution_count": 248, 1637 | "metadata": {}, 1638 | "output_type": "execute_result" 1639 | } 1640 | ], 1641 | "source": [ 1642 | "# Грузим датасет с ирисами Фишера\n", 1643 | "iris_ds = datasets.load_iris()\n", 1644 | "# Создаём датафрэйм\n", 1645 | "df = pd.DataFrame(iris_ds.data, columns=iris_ds.feature_names)\n", 1646 | "# Добавляем таргет\n", 1647 | "df['class'] = iris_ds.target\n", 1648 | "# Выводим соответствие таргета названию\n", 1649 | "print(*[(t,n) for t,n in enumerate(iris_ds.target_names)])\n", 1650 | "# Выводим случайные 5 строк датафрэйма\n", 1651 | "df.sample(5)" 1652 | ] 1653 | }, 1654 | { 1655 | "cell_type": "markdown", 1656 | "metadata": {}, 1657 | "source": [ 1658 | "- Нам нужно будет считать метрики. В большинстве случаев это Евклидова метрика. Но в общем случае это метрика Минковского. \n", 1659 | "
$\\large D(X,Y) = (\\sum\\limits_{i=1}^{n} |x_i-y_i|^p)^{1/p}$\n", 1660 | "- Для некоторых задач используют косинусное расстояние.\n", 1661 | "
$\\large D_C(A,B) = 1- \\frac {\\sum\\limits_{i=1}^{n}A_i \\cdot B_i}{\\sqrt{\\sum\\limits_{i=1}^{n}A_i^2} \\cdot \\sqrt{\\sum\\limits_{i=1}^{n}B_i^2}}$" 1662 | ] 1663 | }, 1664 | { 1665 | "cell_type": "code", 1666 | "execution_count": 379, 1667 | "metadata": {}, 1668 | "outputs": [], 1669 | "source": [ 1670 | "# Для удобства напишем отдельные функции для метрики Минковского и косинусного сходства.\n", 1671 | "\n", 1672 | "def my_minkowski(a,b,p):\n", 1673 | " return sum(abs(a-b)**p)**(1/p)\n", 1674 | "\n", 1675 | "def my_cosine(a,b):\n", 1676 | " return 1 - (a@b)/(sum(a**2)**0.5*sum(b**2)**0.5)" 1677 | ] 1678 | }, 1679 | { 1680 | "cell_type": "code", 1681 | "execution_count": 380, 1682 | "metadata": {}, 1683 | "outputs": [ 1684 | { 1685 | "name": "stdout", 1686 | "output_type": "stream", 1687 | "text": [ 1688 | "True\n", 1689 | "True\n" 1690 | ] 1691 | } 1692 | ], 1693 | "source": [ 1694 | "# проверим всё ли корректно сравнив с реализациями из scipy\n", 1695 | "\n", 1696 | "from scipy.spatial.distance import minkowski, cosine\n", 1697 | "\n", 1698 | "a1 = df.iloc[0,:-1].values\n", 1699 | "a2 = df.iloc[1,:-1].values\n", 1700 | "\n", 1701 | "print(my_minkowski(a1,a2,2) == minkowski(a1,a2,2))\n", 1702 | "print(my_cosine(a1,a2) == cosine(a1,a2))" 1703 | ] 1704 | }, 1705 | { 1706 | "cell_type": "code", 1707 | "execution_count": 381, 1708 | "metadata": {}, 1709 | "outputs": [ 1710 | { 1711 | "data": { 1712 | "text/plain": [ 1713 | "1" 1714 | ] 1715 | }, 1716 | "execution_count": 381, 1717 | "metadata": {}, 1718 | "output_type": "execute_result" 1719 | } 1720 | ], 1721 | "source": [ 1722 | "ttt = pd.Series(data=[3,3,3,1,1,1])\n", 1723 | "ttt.mode()[0]" 1724 | ] 1725 | }, 1726 | { 1727 | "cell_type": "code", 1728 | "execution_count": 442, 1729 | "metadata": {}, 1730 | "outputs": [], 1731 | "source": [ 1732 | "# Подготовим данные.\n", 1733 | "\n", 1734 | "from sklearn.model_selection import train_test_split\n", 1735 | "from sklearn.preprocessing import StandardScaler\n", 1736 | "\n", 1737 | "X = df.drop('class', axis=1)\n", 1738 | "y = df['class']\n", 1739 | "\n", 1740 | "# Разобьём на тренировочную и тестовую выборки\n", 1741 | "X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.25,random_state=1)\n", 1742 | "\n", 1743 | "# Нормализуем\n", 1744 | "scaler = StandardScaler()\n", 1745 | "X_train = scaler.fit_transform(X_train)\n", 1746 | "X_test = scaler.transform(X_test)" 1747 | ] 1748 | }, 1749 | { 1750 | "cell_type": "code", 1751 | "execution_count": 460, 1752 | "metadata": {}, 1753 | "outputs": [], 1754 | "source": [ 1755 | "def my_knn(X_train,X_test,y_train, metric='minkowski', p=1, k=5):\n", 1756 | " # Создаём пустой массив для предсказаний\n", 1757 | " res = np.array([], dtype='int')\n", 1758 | " # Для каждого объекта из тестовой выборки\n", 1759 | " for elem in X_test:\n", 1760 | " # В зависимости от выбранной метрики\n", 1761 | " if metric == 'minkowski':\n", 1762 | " # Считаем дистанции до всех объектов обучающей выборки\n", 1763 | " distances = np.apply_along_axis(lambda x: my_minkowski(x,elem,p), axis=1, arr=X_train)\n", 1764 | " elif metric == 'cosine':\n", 1765 | " distances = np.apply_along_axis(lambda x: my_cosine(x,elem), axis=1, arr=X_train)\n", 1766 | " # Конвертим в pd.Series, индекс задаём как у y_train. Чтобы удобно было сортировать.\n", 1767 | " distances = pd.Series(data=distances,index=y_train.index)\n", 1768 | " # Сортируем дистанции, выбираем k наименьших, по их индексам фильтруем y_train\n", 1769 | " # После ищем моду(наиболее часто встречающееся значение), на случай если их несколько берём только первый элемент\n", 1770 | " y_hat = y_train[distances.sort_values()[:k].index].mode()[0]\n", 1771 | " # Записываем в результирующий массив\n", 1772 | " res = np.append(res,y_hat)\n", 1773 | " return res" 1774 | ] 1775 | }, 1776 | { 1777 | "cell_type": "code", 1778 | "execution_count": 456, 1779 | "metadata": {}, 1780 | "outputs": [ 1781 | { 1782 | "name": "stdout", 1783 | "output_type": "stream", 1784 | "text": [ 1785 | "Manhattan distance: 0.9736842105263158\n", 1786 | "Euclidean distance: 0.9736842105263158\n", 1787 | "Cosine distance: 0.8157894736842105\n" 1788 | ] 1789 | } 1790 | ], 1791 | "source": [ 1792 | "# Тестируем\n", 1793 | "# Манхэттенское расстояние\n", 1794 | "my_res = my_knn(X_train,X_test,y_train)\n", 1795 | "print('Manhattan distance:',accuracy_score(y_test,my_res))\n", 1796 | "\n", 1797 | "# Евклидово расстояние\n", 1798 | "my_res = my_knn(X_train,X_test,y_train,p=2)\n", 1799 | "print('Euclidean distance:', accuracy_score(y_test,my_res))\n", 1800 | "\n", 1801 | "# Косинусное расстояние\n", 1802 | "my_res = my_knn(X_train,X_test,y_train, metric='cosine')\n", 1803 | "print('Cosine distance:', accuracy_score(y_test,my_res))" 1804 | ] 1805 | }, 1806 | { 1807 | "cell_type": "code", 1808 | "execution_count": 459, 1809 | "metadata": {}, 1810 | "outputs": [ 1811 | { 1812 | "name": "stdout", 1813 | "output_type": "stream", 1814 | "text": [ 1815 | "Manhattan distance: 0.9736842105263158\n", 1816 | "Euclidean distance: 0.9736842105263158\n", 1817 | "Cosine distance: 0.8157894736842105\n" 1818 | ] 1819 | } 1820 | ], 1821 | "source": [ 1822 | "# Всё то же самое, но на базе sklearn-овского классификатора\n", 1823 | "\n", 1824 | "# Манхэттенское расстояние\n", 1825 | "clf = KNeighborsClassifier(algorithm='brute',n_neighbors=5, p=1)\n", 1826 | "clf.fit(X_train,y_train)\n", 1827 | "test_res = clf.predict(X_test)\n", 1828 | "print('Manhattan distance:',accuracy_score(y_test,test_res))\n", 1829 | "\n", 1830 | "# Евклидово расстояние\n", 1831 | "clf = KNeighborsClassifier(algorithm='brute',n_neighbors=5, p=2)\n", 1832 | "clf.fit(X_train,y_train)\n", 1833 | "test_res = clf.predict(X_test)\n", 1834 | "print('Euclidean distance:',accuracy_score(y_test,test_res))\n", 1835 | "\n", 1836 | "# Косинусное\n", 1837 | "clf = KNeighborsClassifier(algorithm='brute',n_neighbors=5, metric='cosine')\n", 1838 | "clf.fit(X_train,y_train)\n", 1839 | "test_res = clf.predict(X_test)\n", 1840 | "print('Cosine distance:',accuracy_score(y_test,test_res))" 1841 | ] 1842 | }, 1843 | { 1844 | "cell_type": "markdown", 1845 | "metadata": {}, 1846 | "source": [ 1847 | "**Отлично, все результаты совпадают.**" 1848 | ] 1849 | } 1850 | ], 1851 | "metadata": { 1852 | "interpreter": { 1853 | "hash": "ad2bdc8ecc057115af97d19610ffacc2b4e99fae6737bb82f5d7fb13d2f2c186" 1854 | }, 1855 | "kernelspec": { 1856 | "display_name": "Python 3.8.12 ('base')", 1857 | "language": "python", 1858 | "name": "python3" 1859 | }, 1860 | "language_info": { 1861 | "codemirror_mode": { 1862 | "name": "ipython", 1863 | "version": 3 1864 | }, 1865 | "file_extension": ".py", 1866 | "mimetype": "text/x-python", 1867 | "name": "python", 1868 | "nbconvert_exporter": "python", 1869 | "pygments_lexer": "ipython3", 1870 | "version": "3.8.12" 1871 | }, 1872 | "orig_nbformat": 4 1873 | }, 1874 | "nbformat": 4, 1875 | "nbformat_minor": 2 1876 | } 1877 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Разбираем книгу "Грокаем алгоритмы" и еще много всего дополнительно. 2 | В книге достаточно скупо и мало разобрано, поэтому дополнительно были сделано: 3 | - Два ванианта сортировки пузырьком 4 | - Сортировка вставками 5 | - Сортировка слиянием 6 | - Честный каноничный вариант быстрой сортировки с разбиением Хоара 7 | - Поиск в ширину на аналоге игрового поля 8 | - Алгоритм Дейкстры на аналоге игрового поля 9 | - Несколько простеньких вариантов вычисления чисел Фибоначчи 10 | - Задача о размене монет с бэктрэком 11 | - Задача о рюкзаке также с бэктрэком 12 | - К-ближайших соседей, простой вариант с расчетом всех дистанций с выбором метрики. 13 | --------------------------------------------------------------------------------