{"id":"dsa-graphs-adjacency-lists","slug":"adjacency-lists","title":"Graphs and adjacency lists","text":"Which of your friends' friends haven't you met yet? Can you fly from Lisbon to Osaka with one change of plane? Which of a program's files must Python load before `main.py` can run? Each question is about some things and the links between pairs of them. Put that way, it is a **graph**: a set of **vertices** (the things, also called nodes) and a set of **edges**, each joining two vertices. You have met one kind of graph already, the tree, where every node but the root hangs from exactly one parent. A graph in general has no such rules: it can have loops, several routes between the same two vertices, and pieces with no link between them at all. Interview questions about mazes, flights, schedules and groups of people are very often graph questions in disguise.\n\nLinks come in kinds. A flight has a price and a road a driving time: a number on an edge is its **weight**. The fifth step of this module uses weights; until then, every edge counts as one step. And a link may or may not work both ways. If Ana is Ben's cousin, Ben is Ana's cousin too, so the edge is **undirected**. But Ana can follow Ben online without Ben following back, and a file can import another without being imported by it: those edges are **directed**, from a to b.\n\n### From a list of edges to an adjacency list\n\nA graph often arrives as a list of edges, each a pair of vertices. But the question that graph algorithms ask over and over is \"where can I go from v in one step?\": the **neighbors** of v. Scanning the whole list of edges for every such question costs O(E) each time, for E edges. An **adjacency list** answers at once. It is a dict that maps each vertex to the list of its neighbors:\n\n```python\nroads = [(\"Rome\", \"Naples\"), (\"Florence\", \"Rome\"), (\"Naples\", \"Bari\"), (\"Bologna\", \"Florence\"), (\"Rome\", \"Bari\"), (\"Cagliari\", \"Sassari\")]\n\ngraph = {}\nfor a, b in roads:\n    if a not in graph:\n        graph[a] = []\n    if b not in graph:\n        graph[b] = []\n    graph[a].append(b)\n    graph[b].append(a)\n\nfor city in graph:\n    print(city, len(graph[city]), graph[city])\n```\n\n```output\nRome 3 ['Naples', 'Florence', 'Bari']\nNaples 2 ['Rome', 'Bari']\nFlorence 2 ['Rome', 'Bologna']\nBari 2 ['Naples', 'Rome']\nBologna 1 ['Florence']\nCagliari 1 ['Sassari']\nSassari 1 ['Cagliari']\n```\n\nA road is an undirected edge, so it goes into two lists: Naples is in Rome's list, and Rome is in Naples's. The number of neighbors of a vertex is its **degree**: Rome's is 3. Each edge adds 1 to the degree of both its ends, so the degrees add up to 2E, here 12 for 6 roads. Cagliari and Sassari are on the island of Sardinia, with a road only to each other: a graph can come in separate pieces, and no road leads from Rome to Cagliari.\n\nThe two `if` lines give every vertex a list of its own the first time it shows up, as `a` or as `b`. Without them, the first road to a new city stops with a `KeyError`. (`collections.defaultdict(list)` does the same for you: a missing key gets an empty list the first time it's used.) When the vertices are numbered 0 to n - 1, a list of lists works just as well: `graph[v]` is the list of v's neighbors.\n\n### Directed graphs: out and in\n\nA directed edge goes into one list only, the list of the vertex it leaves from. That list's length is the vertex's **out-degree**. The **in-degree**, the number of edges coming in, needs a count of its own, made in the same single pass over the edges:\n\n```python\nfollows = [(0, 1), (0, 2), (1, 2), (2, 0), (3, 2)]\nn = 4\n\nfollowing = [[] for _ in range(n)]\nfollowers = [0] * n\nfor a, b in follows:\n    following[a].append(b)\n    followers[b] += 1\n\nprint(following)\nprint([len(people) for people in following])\nprint(followers)\n```\n\n```output\n[[1, 2], [2], [0], [2]]\n[2, 1, 1, 1]\n[1, 1, 3, 0]\n```\n\nPerson 2 has three followers and follows one person, and nobody follows 3. Counting each person's followers with a scan of all the edges *per person* gives the same numbers, but costs O(V·E) instead of O(V + E).\n\n### A Python trap: `[[]] * n`\n\nTo make n empty lists, `[[]] * n` looks right. But `*` repeats a reference to *one* list n times, so every vertex shares the same list:\n\n```pycon\n>>> graph = [[]] * 3\n>>> graph[0].append(1)\n>>> graph\n[[1], [1], [1]]\n```\n\nMake each list separately: `[[] for _ in range(n)]`.\n\n### What it costs\n\nAn adjacency list takes O(V + E) memory: a list for each vertex, plus an entry for each directed edge (two for an undirected one). Going through the neighbors of v takes O(deg v), and through the neighbors of every vertex O(V + E). That's what the searches of the next steps do.\n\n\"Is there an edge from a to b?\" is `b in graph[a]`: O(deg a) with a list. With a **set** of neighbors instead, it's O(1) on average, the trade of memory for speed from the step on sets and dicts in the module on complexity. (Sets of strings have no fixed order, so sort them before you print them.) The other classic layout, an **adjacency matrix**, is a V by V table of `True` and `False`, a list of V lists in Python. It answers in O(1) too, but takes O(V²) memory, whatever the number of edges. Take a city's street map: 50,000 crossings, each joined to about 4 others. Its adjacency lists hold about 200,000 entries. Its matrix has 2.5 billion cells, and since every item of a Python list is an 8-byte reference, that's 20 GB, almost all of it `False`. Graphs like this one, with far fewer than V² edges, are called **sparse**, and most real graphs are, so adjacency lists are the usual choice.\n","task":"`build` makes the adjacency list of an undirected graph whose vertices are numbered 0 to n - 1. Fill in the blanks: the edge's way back, and each vertex's degree. The program should print:\n\n```\n[[1, 2], [0, 2], [0, 1, 3], [2], []]\n[2, 2, 3, 1, 0]\n```\n","kind":"fill","seed":"def build(n, edges):\n    graph = [[] for _ in range(n)]\n    for a, b in edges:\n        graph[a].append(b)\n        [[graph[b].append(a)]]\n    return graph\n\n\ngraph = build(5, [(0, 1), (0, 2), (1, 2), (2, 3)])\nprint(graph)\ndegrees = []\nfor neighbors in graph:\n    degrees.append([[len(neighbors)]])\nprint(degrees)\n","solution":"def build(n, edges):\n    graph = [[] for _ in range(n)]\n    for a, b in edges:\n        graph[a].append(b)\n        graph[b].append(a)\n    return graph\n\n\ngraph = build(5, [(0, 1), (0, 2), (1, 2), (2, 3)])\nprint(graph)\ndegrees = []\nfor neighbors in graph:\n    degrees.append(len(neighbors))\nprint(degrees)\n","hints":["An undirected edge goes both ways, so b must get a as a neighbor too.","The first blank is `graph[b].append(a)`. A vertex's degree is the number of its neighbors: `len(neighbors)`."],"tests":[{"name":"Output","stdin":"","expect":"[[1, 2], [0, 2], [0, 1, 3], [2], []]\n[2, 2, 3, 1, 0]","hidden":false}],"require":[],"forbid":[],"more":[{"task":"At a party of n people, numbered 0 to n - 1, `knows` lists pairs `(a, b)`: person a knows person b. A **celebrity** is known by everyone else and knows nobody. Write `find_celebrity(n, knows)` that returns the celebrity's number, or `None` when there is none. No pair appears twice, and nobody is paired with themselves.\n\nIn graph words, `knows` is a directed graph, and the celebrity has an in-degree of n - 1 and an out-degree of 0. Count both in one pass over the pairs: the hidden tests have 100,000 people and over 130,000 pairs, and counting each person's pairs with a scan of the whole list is O(n·E), billions of steps.\n\nFor example, this program:\n\n```python\nprint(find_celebrity(4, [(0, 2), (1, 2), (3, 2), (0, 1), (3, 0)]))\nprint(find_celebrity(3, [(0, 1), (1, 0), (2, 1)]))\n```\n\nprints:\n\n```\n2\nNone\n```\n","kind":"code","seed":"def find_celebrity(n, knows):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(find_celebrity(4, [(0, 2), (1, 2), (3, 2), (0, 1), (3, 0)]))\n    print(find_celebrity(3, [(0, 1), (1, 0), (2, 1)]))\n","solution":"def find_celebrity(n, knows):\n    known_by = [0] * n\n    knows_someone = [False] * n\n    for a, b in knows:\n        known_by[b] += 1\n        knows_someone[a] = True\n    for person in range(n):\n        if known_by[person] == n - 1 and not knows_someone[person]:\n            return person\n    return None\n\n\nif __name__ == \"__main__\":\n    print(find_celebrity(4, [(0, 2), (1, 2), (3, 2), (0, 1), (3, 0)]))\n    print(find_celebrity(3, [(0, 1), (1, 0), (2, 1)]))\n","hints":["Make two lists of n items before the loop: how many people know each person (the in-degree), and whether each person knows someone (the out-degree, or just whether it's above 0).","One loop over the pairs fills both: for `(a, b)`, b is known by one more person, and a knows someone.","Then look for the person whose count is `n - 1` and who knows nobody. If no one fits, return `None`."],"tests":[{"name":"Test 1","stdin":"","call":"print(find_celebrity(4, [(0, 2), (1, 2), (3, 2), (0, 1), (3, 0)]))\nprint(find_celebrity(3, [(0, 1), (1, 0), (2, 1)]))\n","expect":"2\nNone","hidden":false},{"name":"print(find_celebrity(3, [(0, 2), (1, 2), (2, 0)]))","stdin":"","call":"print(find_celebrity(3, [(0, 2), (1, 2), (2, 0)]))\n","expect":"None","hidden":true},{"name":"find_celebrity(3, [(1, 0), (2, 0)])","stdin":"","call":"find_celebrity(3, [(1, 0), (2, 0)])\n","expect":"0","hidden":true,"returns":true},{"name":"print(find_celebrity(4, [(0, 3), (1, 3)]))","stdin":"","call":"print(find_celebrity(4, [(0, 3), (1, 3)]))\n","expect":"None","hidden":true},{"name":"find_celebrity(5, [(i, 4) for i in range(4)] + [(0, 1), (2, 3), (3, 1)])","stdin":"","call":"find_celebrity(5, [(i, 4) for i in range(4)] + [(0, 1), (2, 3), (3, 1)])\n","expect":"4","hidden":true,"returns":true},{"name":"find_celebrity(100_000, [(i, 77_777) for i in range(100_000) if i != 77_777] + [(i, i + 1) for i in range(0, 99_999, 3)])","stdin":"","call":"find_celebrity(100_000, [(i, 77_777) for i in range(100_000) if i != 77_777] + [(i, i + 1) for i in range(0, 99_999, 3)])\n","expect":"77777","hidden":true,"big":true,"returns":true},{"name":"print(find_celebrity(100_000, [(i, 0) for i in range(1, 100_000)] + [(0, 99_999)]))","stdin":"","call":"print(find_celebrity(100_000, [(i, 0) for i in range(1, 100_000)] + [(0, 99_999)]))\n","expect":"None","hidden":true,"big":true}],"require":[],"forbid":[]},{"task":"On a social network, `follows` lists pairs `(a, b)`: person a follows person b. Write `mutual_pairs(follows)` that returns how many pairs of people follow each other. Each such pair counts once. The people are numbers, no pair appears twice in the list, and nobody follows themselves.\n\n`(b, a) in follows` would answer \"does b follow a back?\", but on a list it's a scan, O(E) for each of the E follows: the hidden test has 80,000 follows. Put each person's follows in a set first, so the check is O(1) on average.\n\nFor example, here 1 and 2 follow each other, and so do 3 and 4. So this call:\n\n```python\nprint(mutual_pairs([(1, 2), (2, 1), (1, 3), (3, 4), (4, 3), (2, 3)]))\n```\n\nprints:\n\n```\n2\n```\n","kind":"code","seed":"def mutual_pairs(follows):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(mutual_pairs([(1, 2), (2, 1), (1, 3), (3, 4), (4, 3), (2, 3)]))\n","solution":"def mutual_pairs(follows):\n    following = {}\n    for a, b in follows:\n        if a not in following:\n            following[a] = set()\n        following[a].add(b)\n    count = 0\n    for a, b in follows:\n        if b in following and a in following[b]:\n            count += 1\n    return count // 2\n\n\nif __name__ == \"__main__\":\n    print(mutual_pairs([(1, 2), (2, 1), (1, 3), (3, 4), (4, 3), (2, 3)]))\n","hints":["Build an adjacency list first: a dict that maps each person to the set of people they follow.","Then, for each follow `(a, b)`, check whether b follows a: `a in following[b]`, O(1) on average. Mind people who follow nobody: they aren't keys of the dict.","Every mutual pair is found twice, once from each side, so halve the count at the end. (Or count only the follows with `a < b`.)"],"tests":[{"name":"print(mutual_pairs([(1, 2), (2, 1), (1, 3), (3, 4), (4, 3), (2, 3)]))","stdin":"","call":"print(mutual_pairs([(1, 2), (2, 1), (1, 3), (3, 4), (4, 3), (2, 3)]))\n","expect":"2","hidden":false},{"name":"mutual_pairs([])","stdin":"","call":"mutual_pairs([])\n","expect":"0","hidden":true,"returns":true},{"name":"mutual_pairs([(5, 9), (9, 5)])","stdin":"","call":"mutual_pairs([(5, 9), (9, 5)])\n","expect":"1","hidden":true,"returns":true},{"name":"mutual_pairs([(1, 2), (2, 3), (3, 1)])","stdin":"","call":"mutual_pairs([(1, 2), (2, 3), (3, 1)])\n","expect":"0","hidden":true,"returns":true},{"name":"mutual_pairs([(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1), (4, 1)])","stdin":"","call":"mutual_pairs([(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1), (4, 1)])\n","expect":"3","hidden":true,"returns":true},{"name":"mutual_pairs([(i, (i * 7 + 1) % 60_000) for i in range(60_000)] + [((i * 7 + 1) % 60_000, i) for i in range(0, 60_000, 3)])","stdin":"","call":"mutual_pairs([(i, (i * 7 + 1) % 60_000) for i in range(60_000)] + [((i * 7 + 1) % 60_000, i) for i in range(0, 60_000, 3)])\n","expect":"20000","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[]}]}