├── input ├── demand.csv └── pairwise.csv ├── README.md └── cw_algorithm.ipynb /input/demand.csv: -------------------------------------------------------------------------------- 1 | node,distance to depot,demand 2 | 0,0,0 3 | 1,25,4 4 | 2,43,6 5 | 3,57,5 6 | 4,43,4 7 | 5,61,7 8 | 6,29,3 9 | 7,41,5 10 | 8,48,4 11 | 9,71,4 12 | -------------------------------------------------------------------------------- /input/pairwise.csv: -------------------------------------------------------------------------------- 1 | ,1,2,3,4,5,6,7,8,9 2 | 1,0,29,34,43,68,49,66,72,91 3 | 2,29,0,52,72,96,72,81,89,114 4 | 3,34,52,0,45,71,71,95,99,108 5 | 4,43,72,45,0,27,36,65,65,65 6 | 5,68,96,71,27,0,40,66,62,46 7 | 6,49,72,71,36,40,0,31,31,43 8 | 7,66,81,95,65,66,31,0,11,46 9 | 8,72,89,99,65,62,31,11,0,36 10 | 9,91,114,108,65,46,43,46,36,0 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vehicle routing problem (VRP): Clarke-Wright Savings Algorithm 2 | Clarke-Wright savings algorithm for vehicle routing problem (VRP) with single depot and vehicle capacity constraints. 3 | 4 | In one of my courses, we were asked to manually solve a vehicle routing problem (a kind of optimization problem) . It turns out I don't want to manually solve it, so I wrote this code. It has each step reported. Feel free to use it for your own purpose. 5 | 6 | A complete description of Single Depot VRP can be found here: 7 | http://web.mit.edu/urban_or_book/www/book/chapter6/6.4.12.html 8 | 9 | In order to get the code run, you need 2 input files in .csv format: 10 | - Nodes' distances to depot and the demands on them. 11 | - Pairwise distances between nodes. If your nodes are in coordinate format, you can actually convert them to pairwise easily using other tools. 12 | 13 | I have the sample data files uploaded here. They correspond to the textbook link above, so that you can check if the results are correct. 14 | 15 | 16 | -------------------------------------------------------------------------------- /cw_algorithm.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Reference: http://web.mit.edu/urban_or_book/www/book/chapter6/6.4.12.html\n", 8 | "\n", 9 | "Clark-Wright Savings Algorithm (Algorithm 6.7)\n", 10 | "- STEP \t1: \tCalculate the savings s(i, j) = d(D, i) + d(D, j) - d(i, j) for every pair (i, j) of demand nodes.\n", 11 | "- STEP \t2: \tRank the savings s(i, j) and list them in descending order of magnitude. This creates the \"savings list.\" Process the savings list beginning with the topmost entry in the list (the largest s(i, j)).\n", 12 | "- STEP \t3: \tFor the savings s(i, j) under consideration, include link (i, j) in a route if no route constraints will be violated through the inclusion of (i, j) in a route, and if:\n", 13 | "\n", 14 | " a. Either, neither i nor j have already been assigned to a route, in which case a new route is initiated including both i and j.\n", 15 | "\n", 16 | " b. Or, exactly one of the two nodes (i or j) has already been included in an existing route and that point is not interior to that route (a point is interior to a route if it is not adjacent to the depot D in the order of traversal of nodes), in which case the link (i, j) is added to that same route.\n", 17 | "\n", 18 | " c. Or, both i and j have already been included in two different existing routes and neither point is interior to its route, in which case the two routes are merged.\n", 19 | " \n", 20 | "\n", 21 | "- STEP \t4: \tIf the savings list s(i, j) has not been exhausted, return to Step 3, processing the next entry in the list; otherwise, stop: the solution to the VRP consists of the routes created during Step 3. (Any nodes that have not been assigned to a route during Step 3 must each be served by a vehicle route that begins at the depot D visits the unassigned point and returns to D.) " 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "import numpy as np\n", 31 | "import pandas as pd" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "metadata": {}, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/html": [ 42 | "
\n", 43 | "\n", 56 | "\n", 57 | " \n", 58 | " \n", 59 | " \n", 60 | " \n", 61 | " \n", 62 | " \n", 63 | " \n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | "
d0demand
node
000
1254
2436
3575
4434
\n", 97 | "
" 98 | ], 99 | "text/plain": [ 100 | " d0 demand\n", 101 | "node \n", 102 | "0 0 0\n", 103 | "1 25 4\n", 104 | "2 43 6\n", 105 | "3 57 5\n", 106 | "4 43 4" 107 | ] 108 | }, 109 | "execution_count": 2, 110 | "metadata": {}, 111 | "output_type": "execute_result" 112 | } 113 | ], 114 | "source": [ 115 | "# read node data in coordinate (x,y) format\n", 116 | "nodes = pd.read_csv('input/demand.csv', index_col = 'node')\n", 117 | "nodes.rename(columns={\"distance to depot\":'d0'}, inplace = True)\n", 118 | "node_number = len(nodes.index) - 1\n", 119 | "nodes.head()" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": 3, 125 | "metadata": {}, 126 | "outputs": [ 127 | { 128 | "data": { 129 | "text/html": [ 130 | "
\n", 131 | "\n", 144 | "\n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | "
123456789
102934436849667291
2290527296728189114
3345204571719599108
443724502736656565
568967127040666246
649727136400313143
766819565663101146
872899965623111036
99111410865464346360
\n", 282 | "
" 283 | ], 284 | "text/plain": [ 285 | " 1 2 3 4 5 6 7 8 9\n", 286 | " \n", 287 | "1 0 29 34 43 68 49 66 72 91\n", 288 | "2 29 0 52 72 96 72 81 89 114\n", 289 | "3 34 52 0 45 71 71 95 99 108\n", 290 | "4 43 72 45 0 27 36 65 65 65\n", 291 | "5 68 96 71 27 0 40 66 62 46\n", 292 | "6 49 72 71 36 40 0 31 31 43\n", 293 | "7 66 81 95 65 66 31 0 11 46\n", 294 | "8 72 89 99 65 62 31 11 0 36\n", 295 | "9 91 114 108 65 46 43 46 36 0" 296 | ] 297 | }, 298 | "execution_count": 3, 299 | "metadata": {}, 300 | "output_type": "execute_result" 301 | } 302 | ], 303 | "source": [ 304 | "# read pairwise distance\n", 305 | "pw = pd.read_csv('input/pairwise.csv', index_col = 'Unnamed: 0')\n", 306 | "pw.index.rename('',inplace = True)\n", 307 | "pw" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 4, 313 | "metadata": {}, 314 | "outputs": [ 315 | { 316 | "data": { 317 | "text/html": [ 318 | "
\n", 319 | "\n", 332 | "\n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | "
saving
(9,5)86
(9,8)83
(8,7)78
(5,4)77
(9,7)66
\n", 362 | "
" 363 | ], 364 | "text/plain": [ 365 | " saving\n", 366 | "(9,5) 86\n", 367 | "(9,8) 83\n", 368 | "(8,7) 78\n", 369 | "(5,4) 77\n", 370 | "(9,7) 66" 371 | ] 372 | }, 373 | "execution_count": 4, 374 | "metadata": {}, 375 | "output_type": "execute_result" 376 | } 377 | ], 378 | "source": [ 379 | "# calculate savings for each link\n", 380 | "savings = dict()\n", 381 | "for r in pw.index:\n", 382 | " for c in pw.columns:\n", 383 | " if int(c) != int(r): \n", 384 | " a = max(int(r), int(c))\n", 385 | " b = min(int(r), int(c))\n", 386 | " key = '(' + str(a) + ',' + str(b) + ')'\n", 387 | " savings[key] = nodes['d0'][int(r)] + nodes['d0'][int(c)] - pw[c][r]\n", 388 | "\n", 389 | "# put savings in a pandas dataframe, and sort by descending\n", 390 | "sv = pd.DataFrame.from_dict(savings, orient = 'index')\n", 391 | "sv.rename(columns = {0:'saving'}, inplace = True)\n", 392 | "sv.sort_values(by = ['saving'], ascending = False, inplace = True)\n", 393 | "sv.head()" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": 5, 399 | "metadata": {}, 400 | "outputs": [], 401 | "source": [ 402 | "# convert link string to link list to handle saving's key, i.e. str(10, 6) to (10, 6)\n", 403 | "def get_node(link):\n", 404 | " link = link[1:]\n", 405 | " link = link[:-1]\n", 406 | " nodes = link.split(',')\n", 407 | " return [int(nodes[0]), int(nodes[1])]" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 6, 413 | "metadata": {}, 414 | "outputs": [], 415 | "source": [ 416 | "# determine if a node is interior to a route\n", 417 | "def interior(node, route):\n", 418 | " try:\n", 419 | " i = route.index(node)\n", 420 | " # adjacent to depot, not interior\n", 421 | " if i == 0 or i == (len(route) - 1):\n", 422 | " label = False\n", 423 | " else:\n", 424 | " label = True\n", 425 | " except:\n", 426 | " label = False\n", 427 | " \n", 428 | " return label" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 7, 434 | "metadata": {}, 435 | "outputs": [], 436 | "source": [ 437 | "# merge two routes with a connection link\n", 438 | "def merge(route0, route1, link):\n", 439 | " if route0.index(link[0]) != (len(route0) - 1):\n", 440 | " route0.reverse()\n", 441 | " \n", 442 | " if route1.index(link[1]) != 0:\n", 443 | " route1.reverse()\n", 444 | " \n", 445 | " return route0 + route1" 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": 8, 451 | "metadata": {}, 452 | "outputs": [], 453 | "source": [ 454 | "# sum up to obtain the total passengers belonging to a route\n", 455 | "def sum_cap(route):\n", 456 | " sum_cap = 0\n", 457 | " for node in route:\n", 458 | " sum_cap += nodes.demand[node]\n", 459 | " return sum_cap" 460 | ] 461 | }, 462 | { 463 | "cell_type": "code", 464 | "execution_count": 9, 465 | "metadata": {}, 466 | "outputs": [], 467 | "source": [ 468 | "# determine 4 things:\n", 469 | "# 1. if the link in any route in routes -> determined by if count_in > 0\n", 470 | "# 2. if yes, which node is in the route -> returned to node_sel\n", 471 | "# 3. if yes, which route is the node belongs to -> returned to route id: i_route\n", 472 | "# 4. are both of the nodes in the same route? -> overlap = 1, yes; otherwise, no\n", 473 | "def which_route(link, routes):\n", 474 | " # assume nodes are not in any route\n", 475 | " node_sel = list()\n", 476 | " i_route = [-1, -1]\n", 477 | " count_in = 0\n", 478 | " \n", 479 | " for route in routes:\n", 480 | " for node in link:\n", 481 | " try:\n", 482 | " route.index(node)\n", 483 | " i_route[count_in] = routes.index(route)\n", 484 | " node_sel.append(node)\n", 485 | " count_in += 1\n", 486 | " except:\n", 487 | " pass\n", 488 | " \n", 489 | " if i_route[0] == i_route[1]:\n", 490 | " overlap = 1\n", 491 | " else:\n", 492 | " overlap = 0\n", 493 | " \n", 494 | " return node_sel, count_in, i_route, overlap" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": 10, 500 | "metadata": {}, 501 | "outputs": [ 502 | { 503 | "name": "stdout", 504 | "output_type": "stream", 505 | "text": [ 506 | "step 1 :\n", 507 | "\t Link [9, 5] fulfills criteria a), so it is created as a new route\n", 508 | "\t route: [9, 5] with load 11\n", 509 | "step 2 :\n", 510 | "\t Link [9, 8] fulfills criteria b), so a new node is added to route [9, 5] .\n", 511 | "\t route: [8, 9, 5] with load 15\n", 512 | "step 3 :\n", 513 | "\t Link [8, 7] fulfills criteria b), so a new node is added to route [8, 9, 5] .\n", 514 | "\t route: [7, 8, 9, 5] with load 20\n", 515 | "step 4 :\n", 516 | "\t Though Link [5, 4] fulfills criteria b), it exceeds maximum load, so skip this link.\n", 517 | "step 5 :\n", 518 | "\t Link [9, 7] is already included in the routes\n", 519 | "step 6 :\n", 520 | "\t For Link [9, 6] , node 9 is interior to route [7, 8, 9, 5] , so skip this link\n", 521 | "step 7 :\n", 522 | "\t Link [4, 3] fulfills criteria a), so it is created as a new route\n", 523 | "\t route: [7, 8, 9, 5] with load 20\n", 524 | "\t route: [4, 3] with load 9\n", 525 | "step 8 :\n", 526 | "\t Link [6, 5] fulfills criteria b), so a new node is added to route [7, 8, 9, 5] .\n", 527 | "\t route: [7, 8, 9, 5, 6] with load 23\n", 528 | "\t route: [4, 3] with load 9\n", 529 | "step 9 :\n", 530 | "\t For link [9, 4] , Two nodes are found in two different routes, but not all the nodes fulfill interior requirement, so skip this link\n", 531 | "step 10 :\n", 532 | "\t Link [3, 2] fulfills criteria b), so a new node is added to route [4, 3] .\n", 533 | "\t route: [7, 8, 9, 5, 6] with load 23\n", 534 | "\t route: [4, 3, 2] with load 15\n", 535 | "step 11 :\n", 536 | "\t For Link [3, 1] , node 3 is interior to route [4, 3, 2] , so skip this link\n", 537 | "step 12 :\n", 538 | "\t Link [8, 5] is already included in the routes\n", 539 | "step 13 :\n", 540 | "\t For link [5, 3] , Two nodes are found in two different routes, but not all the nodes fulfill interior requirement, so skip this link\n", 541 | "step 14 :\n", 542 | "\t Link [8, 6] is already included in the routes\n", 543 | "step 15 :\n", 544 | "\t Link [7, 6] is already included in the routes\n", 545 | "step 16 :\n", 546 | "\t Link [2, 1] fulfills criteria b), so a new node is added to route [4, 3, 2] .\n", 547 | "\t route: [7, 8, 9, 5, 6] with load 23\n", 548 | "\t route: [4, 3, 2, 1] with load 19\n", 549 | "-------\n", 550 | "All nodes are included in the routes, algorithm closed\n", 551 | "------\n", 552 | "Routes found are: \n" 553 | ] 554 | }, 555 | { 556 | "data": { 557 | "text/plain": [ 558 | "[[0, 7, 8, 9, 5, 6, 0], [0, 4, 3, 2, 1, 0]]" 559 | ] 560 | }, 561 | "execution_count": 10, 562 | "metadata": {}, 563 | "output_type": "execute_result" 564 | } 565 | ], 566 | "source": [ 567 | "# create empty routes\n", 568 | "routes = list()\n", 569 | "\n", 570 | "# if there is any remaining customer to be served\n", 571 | "remaining = True\n", 572 | "\n", 573 | "# define capacity of the vehicle\n", 574 | "cap = 23\n", 575 | "\n", 576 | "# record steps\n", 577 | "step = 0\n", 578 | "\n", 579 | "# get a list of nodes, excluding the depot\n", 580 | "node_list = list(nodes.index)\n", 581 | "node_list.remove(0)\n", 582 | "\n", 583 | "# run through each link in the saving list\n", 584 | "for link in sv.index:\n", 585 | " step += 1\n", 586 | " if remaining:\n", 587 | "\n", 588 | " print('step ', step, ':')\n", 589 | "\n", 590 | " link = get_node(link)\n", 591 | " node_sel, num_in, i_route, overlap = which_route(link, routes)\n", 592 | "\n", 593 | " # condition a. Either, neither i nor j have already been assigned to a route, \n", 594 | " # ...in which case a new route is initiated including both i and j.\n", 595 | " if num_in == 0:\n", 596 | " if sum_cap(link) <= cap:\n", 597 | " routes.append(link)\n", 598 | " node_list.remove(link[0])\n", 599 | " node_list.remove(link[1])\n", 600 | " print('\\t','Link ', link, ' fulfills criteria a), so it is created as a new route')\n", 601 | " else:\n", 602 | " print('\\t','Though Link ', link, ' fulfills criteria a), it exceeds maximum load, so skip this link.')\n", 603 | "\n", 604 | " # condition b. Or, exactly one of the two nodes (i or j) has already been included \n", 605 | " # ...in an existing route and that point is not interior to that route \n", 606 | " # ...(a point is interior to a route if it is not adjacent to the depot D in the order of traversal of nodes), \n", 607 | " # ...in which case the link (i, j) is added to that same route. \n", 608 | " elif num_in == 1:\n", 609 | " n_sel = node_sel[0]\n", 610 | " i_rt = i_route[0]\n", 611 | " position = routes[i_rt].index(n_sel)\n", 612 | " link_temp = link.copy()\n", 613 | " link_temp.remove(n_sel)\n", 614 | " node = link_temp[0]\n", 615 | "\n", 616 | " cond1 = (not interior(n_sel, routes[i_rt]))\n", 617 | " cond2 = (sum_cap(routes[i_rt] + [node]) <= cap)\n", 618 | "\n", 619 | " if cond1:\n", 620 | " if cond2:\n", 621 | " print('\\t','Link ', link, ' fulfills criteria b), so a new node is added to route ', routes[i_rt], '.')\n", 622 | " if position == 0:\n", 623 | " routes[i_rt].insert(0, node)\n", 624 | " else:\n", 625 | " routes[i_rt].append(node)\n", 626 | " node_list.remove(node)\n", 627 | " else:\n", 628 | " print('\\t','Though Link ', link, ' fulfills criteria b), it exceeds maximum load, so skip this link.')\n", 629 | " continue\n", 630 | " else:\n", 631 | " print('\\t','For Link ', link, ', node ', n_sel, ' is interior to route ', routes[i_rt], ', so skip this link')\n", 632 | " continue\n", 633 | "\n", 634 | " # condition c. Or, both i and j have already been included in two different existing routes \n", 635 | " # ...and neither point is interior to its route, in which case the two routes are merged. \n", 636 | " else:\n", 637 | " if overlap == 0:\n", 638 | " cond1 = (not interior(node_sel[0], routes[i_route[0]]))\n", 639 | " cond2 = (not interior(node_sel[1], routes[i_route[1]]))\n", 640 | " cond3 = (sum_cap(routes[i_route[0]] + routes[i_route[1]]) <= cap)\n", 641 | "\n", 642 | " if cond1 and cond2:\n", 643 | " if cond3:\n", 644 | " route_temp = merge(routes[i_route[0]], routes[i_route[1]], node_sel)\n", 645 | " temp1 = routes[i_route[0]]\n", 646 | " temp2 = routes[i_route[1]]\n", 647 | " routes.remove(temp1)\n", 648 | " routes.remove(temp2)\n", 649 | " routes.append(route_temp)\n", 650 | " try:\n", 651 | " node_list.remove(link[0])\n", 652 | " node_list.remove(link[1])\n", 653 | " except:\n", 654 | " #print('\\t', f\"Node {link[0]} or {link[1]} has been removed in a previous step.\")\n", 655 | " pass\n", 656 | " print('\\t','Link ', link, ' fulfills criteria c), so route ', temp1, ' and route ', temp2, ' are merged')\n", 657 | " else:\n", 658 | " print('\\t','Though Link ', link, ' fulfills criteria c), it exceeds maximum load, so skip this link.')\n", 659 | " continue\n", 660 | " else:\n", 661 | " print('\\t','For link ', link, ', Two nodes are found in two different routes, but not all the nodes fulfill interior requirement, so skip this link')\n", 662 | " continue\n", 663 | " else:\n", 664 | " print('\\t','Link ', link, ' is already included in the routes')\n", 665 | " continue\n", 666 | "\n", 667 | " for route in routes: \n", 668 | " print('\\t','route: ', route, ' with load ', sum_cap(route))\n", 669 | " else:\n", 670 | " print('-------')\n", 671 | " print('All nodes are included in the routes, algorithm closed')\n", 672 | " break\n", 673 | "\n", 674 | " remaining = bool(len(node_list) > 0)\n", 675 | "\n", 676 | "# check if any node is left, assign to a unique route\n", 677 | "for node_o in node_list:\n", 678 | " routes.append([node_o])\n", 679 | "\n", 680 | "# add depot to the routes\n", 681 | "for route in routes:\n", 682 | " route.insert(0,0)\n", 683 | " route.append(0)\n", 684 | "\n", 685 | "print('------')\n", 686 | "print('Routes found are: ')\n", 687 | "\n", 688 | "routes" 689 | ] 690 | }, 691 | { 692 | "cell_type": "markdown", 693 | "metadata": {}, 694 | "source": [ 695 | "### Check this result with the textbook link above. Remember to offset by 1." 696 | ] 697 | }, 698 | { 699 | "cell_type": "code", 700 | "execution_count": null, 701 | "metadata": {}, 702 | "outputs": [], 703 | "source": [] 704 | } 705 | ], 706 | "metadata": { 707 | "kernelspec": { 708 | "display_name": "Python 3", 709 | "language": "python", 710 | "name": "python3" 711 | }, 712 | "language_info": { 713 | "codemirror_mode": { 714 | "name": "ipython", 715 | "version": 3 716 | }, 717 | "file_extension": ".py", 718 | "mimetype": "text/x-python", 719 | "name": "python", 720 | "nbconvert_exporter": "python", 721 | "pygments_lexer": "ipython3", 722 | "version": "3.8.8" 723 | } 724 | }, 725 | "nbformat": 4, 726 | "nbformat_minor": 4 727 | } 728 | --------------------------------------------------------------------------------