{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "This notebook was prepared by [Donne Martin](https://github.com/donnemartin). Source and license info is on [GitHub](https://github.com/donnemartin/interactive-coding-challenges)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Solution Notebook" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Problem: Find a build order given a list of projects and dependencies.\n", "\n", "* [Constraints](#Constraints)\n", "* [Test Cases](#Test-Cases)\n", "* [Algorithm](#Algorithm)\n", "* [Code](#Code)\n", "* [Unit Test](#Unit-Test)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Constraints\n", "\n", "* Is it possible to have a cyclic graph as the input?\n", " * Yes\n", "* Can we assume we already have Graph and Node classes?\n", " * Yes\n", "* Can we assume this is a connected graph?\n", " * Yes\n", "* Can we assume the inputs are valid?\n", " * Yes\n", "* Can we assume this fits memory?\n", " * Yes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Test Cases\n", "\n", "* projects: a, b, c, d, e, f, g\n", "* dependencies: (d, g), (f, c), (f, b), (f, a), (c, a), (b, a), (a, e), (b, e)\n", "* output: d, f, c, b, g, a, e\n", "\n", "Note: Edge direction is down\n", "
\n",
    "    f     d\n",
    "   /|\\    |\n",
    "  c | b   g\n",
    "   \\|/|\n",
    "    a |\n",
    "    |/\n",
    "    e\n",
    "
\n", "\n", "Test a graph with a cycle, output should be None" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Algorithm\n", "\n", "We can determine the build order using a topological sort.\n", "\n", "* Build the graph with projects (nodes) and dependencies (directed edges)\n", "* Add initially non-dependent nodes to processed_nodes\n", " * If none exist, we have a circular dependency, return None\n", "* While the length processed_nodes < the length of graph nodes\n", " * Remove outgoing edges from newly added items in processed_nodes\n", " * Add non-dependent nodes to processed_nodes\n", " * If we didn't add any nodes, we have a circular dependency, return None\n", "* Return processed_nodes\n", "\n", "Complexity:\n", "* Time: O(V + E)\n", "* Space: O(V + E)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Code" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from collections import deque\n", "\n", "\n", "class Dependency(object):\n", "\n", " def __init__(self, node_key_before, node_key_after):\n", " self.node_key_before = node_key_before\n", " self.node_key_after = node_key_after" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "%run ../graph/graph.py" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class BuildOrder(object):\n", "\n", " def __init__(self, dependencies):\n", " self.dependencies = dependencies\n", " self.graph = Graph()\n", " self._build_graph()\n", "\n", " def _build_graph(self):\n", " for dependency in self.dependencies:\n", " self.graph.add_edge(dependency.node_key_before,\n", " dependency.node_key_after)\n", "\n", " def _find_start_nodes(self, processed_nodes):\n", " nodes_to_process = {}\n", " for key, node in self.graph.nodes.items():\n", " if node.incoming_edges == 0 and key not in processed_nodes:\n", " nodes_to_process[key] = node\n", " return nodes_to_process\n", "\n", " def _process_nodes(self, nodes_to_process, processed_nodes):\n", " for node in nodes_to_process.values():\n", " # We'll need to iterate on copies since we'll need\n", " # to change the dictionaries during iteration with\n", " # the remove_neighbor call\n", " for adj_node in list(node.adj_nodes.values()):\n", " node.remove_neighbor(adj_node)\n", " processed_nodes[node.key] = node\n", " nodes_to_process = {}\n", "\n", " def find_build_order(self):\n", " result = []\n", " nodes_to_process = {}\n", " processed_nodes = {}\n", " while len(result) != len(self.graph.nodes):\n", " nodes_to_process = self._find_start_nodes(processed_nodes)\n", " if not nodes_to_process:\n", " return None\n", " result.extend(nodes_to_process.values())\n", " self._process_nodes(nodes_to_process, processed_nodes)\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Unit Test" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "%run ../utils/results.py" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting test_build_order.py\n" ] } ], "source": [ "%%writefile test_build_order.py\n", "import unittest\n", "\n", "\n", "class TestBuildOrder(unittest.TestCase):\n", "\n", " def __init__(self, *args, **kwargs):\n", " super(TestBuildOrder, self).__init__()\n", " self.dependencies = [\n", " Dependency('d', 'g'),\n", " Dependency('f', 'c'),\n", " Dependency('f', 'b'),\n", " Dependency('f', 'a'),\n", " Dependency('c', 'a'),\n", " Dependency('b', 'a'),\n", " Dependency('a', 'e'),\n", " Dependency('b', 'e'),\n", " ]\n", "\n", " def test_build_order(self):\n", " build_order = BuildOrder(self.dependencies)\n", " processed_nodes = build_order.find_build_order()\n", "\n", " expected_result0 = ('d', 'f')\n", " expected_result1 = ('c', 'b', 'g')\n", " self.assertTrue(processed_nodes[0].key in expected_result0)\n", " self.assertTrue(processed_nodes[1].key in expected_result0)\n", " self.assertTrue(processed_nodes[2].key in expected_result1)\n", " self.assertTrue(processed_nodes[3].key in expected_result1)\n", " self.assertTrue(processed_nodes[4].key in expected_result1)\n", " self.assertTrue(processed_nodes[5].key is 'a')\n", " self.assertTrue(processed_nodes[6].key is 'e')\n", "\n", " print('Success: test_build_order')\n", "\n", " def test_build_order_circular(self):\n", " self.dependencies.append(Dependency('e', 'f'))\n", " build_order = BuildOrder(self.dependencies)\n", " processed_nodes = build_order.find_build_order()\n", " self.assertTrue(processed_nodes is None)\n", "\n", " print('Success: test_build_order_circular')\n", "\n", "\n", "def main():\n", " test = TestBuildOrder()\n", " test.test_build_order()\n", " test.test_build_order_circular()\n", "\n", "\n", "if __name__ == '__main__':\n", " main()" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success: test_build_order\n", "Success: test_build_order_circular\n" ] } ], "source": [ "%run -i test_build_order.py" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.2" } }, "nbformat": 4, "nbformat_minor": 1 }