We closed the previous module with a confession: all our trees hid a silent assumption — that every node has exactly one parent. The moment a TaskFlow task depends on several others at once — "deploy API" needs both "migrate DB" and "configure server" to finish — the hierarchy falls short. And if someone accidentally creates a circular dependency on top of that, the tree simply cannot represent it. The structure that can handle all of this is the graph: the most general one in the course, the one that subsumes lists and trees as special cases, and the one that models social networks, maps, the web, and a project's dependencies. In this lesson we'll learn its vocabulary; still no representation code (that comes in the next lesson) and no algorithms (they're waiting in 07-03 with the visited promised back in module 5).
Contents
- From tree to graph: why we need something more general
- Formal definition: vertices and edges
- Essential terminology (with diagrams)
- The tree as a special case of graph
- Modeling TaskFlow's dependencies as a directed graph
- Graphs in the real world
From tree to graph: why we need something more general
Let's recall the two concrete limitations we ran into at the end of module 6:
- Multiple parents: in a tree, "deploy API" could only hang from "migrate DB" or from "configure server", never from both. But in a real project it depends on both.
- Possible cycles: nothing stops a TaskFlow user from declaring that A depends on B, B on C, and C on A. A tree can't even express that; a graph can, and it will also give us algorithms to detect it and warn the user.
A graph removes both restrictions: any node can connect to any other, in any direction, even forming cycles. In exchange, we lose the tree's comfortable guarantees (a single root, no cycles, a single path between any two nodes), which is why graph algorithms will have to be more careful. That's the price of generality.
Formal definition: vertices and edges
A graph is a pair G = (V, E) where:
Vis a set of vertices (also called nodes): the entities. In TaskFlow, the tasks.Eis a set of edges: the relationships between pairs of vertices. In TaskFlow, the dependencies.
For example, with three tasks:
V = { migrate_db, configure_server, deploy_api }
E = { (migrate_db, deploy_api), (configure_server, deploy_api) }Notice that here the edges are ordered pairs: (migrate_db, deploy_api) is not the same as (deploy_api, migrate_db). That takes us straight into the terminology.
Essential terminology (with diagrams)
Directed and undirected
- In a directed graph (or digraph), every edge has a direction: it goes from a source to a target and is drawn with an arrow. "Migrating the DB unblocks deploying the API" is not symmetric.
- In an undirected graph, the edge is mutual: if A is connected to B, B is connected to A. "Anna and Bruno are friends".
graph LR
subgraph Directed
A[migrate_db] --> B[deploy_api]
end
subgraph Undirected
C[Anna] --- D[Bruno]
end
Weighted and unweighted
A graph is weighted when every edge carries a number (its weight): cost, distance, hours... In TaskFlow, an edge's weight will be the hours it takes to complete the transition between two tasks. If all we care about is whether there's a connection, the graph is unweighted.
graph LR
A[design_schema] -->|3 h| B[migrate_db]
B -->|2 h| C[deploy_api]
Degree of a vertex
The degree measures how many edges touch a vertex. In directed graphs it splits in two:
| Concept | Definition | Reading in TaskFlow |
|---|---|---|
| In-degree | Edges that arrive at the vertex | How many tasks this task must wait for |
| Out-degree | Edges that leave the vertex | How many tasks it unblocks when it finishes |
| Degree (undirected) | Incident edges | Number of connections |
In the "deploy API" diagram: in-degree 2 (it waits for two tasks), out-degree whatever it unblocks afterwards.
Path and cycle
- A path is a sequence of vertices connected by consecutive edges:
design_schema → migrate_db → deploy_api. Its length is its number of edges (2 in the example). - A cycle is a path that starts and ends at the same vertex without repeating edges. In a dependency graph, a cycle is a fatal error: nobody can start.
graph LR
A[design_ui] --> B[implement_ui]
B --> C[review_docs]
C --> A
style A fill:#fdd,stroke:#c00
style B fill:#fdd,stroke:#c00
style C fill:#fdd,stroke:#c00
This cycle (design_ui → implement_ui → review_docs → design_ui) means none of the three tasks can ever get off the ground. Does "cycle detection" ring a bell? In module 2 we detected cycles in linked lists with Floyd's algorithm; in graphs we'll need a different technique (lesson 07-03), but the problem is a sibling.
Connected graphs and connected components
An undirected graph is connected if you can reach any vertex from any other. If not, it splits into connected components: "islands" of vertices connected among themselves but isolated from the rest. In TaskFlow, two components are two projects that share no dependency at all.
graph LR
subgraph Component 1
A[task_a] --- B[task_b] --- C[task_c]
end
subgraph Component 2
D[task_d] --- E[task_e]
end
Dense and sparse
- A dense graph has many edges: close to the maximum possible, which with
ndirected vertices isn · (n - 1). - A sparse graph has few: on the order of
n.
Real dependency graphs are almost always sparse (each task depends on 1–3 tasks, not on all 200 in the project). This detail, which looks minor, will decide in the next lesson how the graph should be represented in memory.
DAG: directed acyclic graph
A DAG (Directed Acyclic Graph) is a directed graph with no cycles. It's the star of this module: a well-formed TaskFlow project is exactly a DAG. It allows multiple parents (unlike the tree) but forbids vicious circles (unlike the general graph).
graph LR
A[design_schema] --> B[migrate_db]
S[configure_server] --> C
B --> C[deploy_api]
C --> D[integration_tests]
U[design_ui] --> I[implement_ui]
I --> D
D --> L[launch]
Observe: "deploy_api" and "integration_tests" have multiple parents (impossible in a tree) and yet there is no cycle anywhere. This DAG will be the recurring example of the whole module.
The tree as a special case of graph
Now we can place module 6 on the map: a tree is a connected, acyclic graph (and in its rooted version, directed with in-degree 1 at every vertex except the root, which has 0). The course's entire hierarchy of structures fits:
| Structure | As a graph |
|---|---|
| Linked list (module 2) | Directed graph where every vertex has out-degree ≤ 1: a path |
| Circular list (module 2) | A simple cycle |
| Tree (module 6) | Connected acyclic graph; rooted: each node, a single parent |
| DAG | Directed with no cycles; multiple parents allowed |
| General graph | No restrictions |
Each row relaxes one restriction from the previous one. The algorithms we'll see work on general graphs and therefore also on every special case: BFS on a tree is exactly the level-order traversal from module 6.
Modeling TaskFlow's dependencies as a directed graph
One detail remains to pin down, and we'll carry it through the whole module: which way the arrows point. There are two possible conventions and both are legitimate; the important thing is to pick one and never mix them.
- Option A:
A → Bmeans "A depends on B". - Option B:
A → Bmeans "B depends on A" (A must finish first; the arrow points at what A unblocks).
In this course we adopt option B: the edge migrate_db → deploy_api reads "finishing migrate_db brings deploy_api closer to being unblocked". It's the natural convention for the algorithms ahead: following the arrows means moving forward in project time, and the "valid execution order" will come from walking them forward.
With this convention, and remembering that every TaskFlow task is still the usual dict (id, title, priority, status, assigned_to, tags, hours), the complete mental model is:
- Vertex: a task's
id(the fulldictlives elsewhere, in anid → taskdictionary, as in module 5's index). - Edge
A → B: B depends on A. - In-degree of B: number of B's pending dependencies. When it reaches 0... B is runnable. Hold on to this idea: it's the seed of topological order (07-03).
- Edge weight (when there is one): hours or cost of the transition (07-04).
Graphs in the real world
The same vocabulary models very different systems; only what counts as a vertex and what counts as an edge changes:
| System | Vertices | Edges | Directed? | Weighted? |
|---|---|---|---|---|
| Social network | People | Friendship / "follows" | Friendship no; "follows" yes | Usually not |
| Road map | Junctions, cities | Road segments | Sometimes (one-way) | Yes (km, minutes) |
| The web | Pages | Links | Yes | No (or relevance) |
Software packages (pip) |
Packages | "requires" | Yes | No |
| TaskFlow | Tasks | Dependencies | Yes | Optional (hours) |
Notice that pip install solves exactly the same problem as TaskFlow: given a dependency DAG, find a valid installation order and complain if there are cycles. When we implement topological order in 07-03, you'll have understood how a package manager works on the inside.
Common Mistakes and Tips
- Mixing the two arrow conventions. If one day you draw
A → Bas "A depends on B" and another day as "B depends on A", your algorithms will produce inverted results. Write the convention down (ours: the arrow points at what gets unblocked) and stick to it. - Confusing a path with an edge. That a path exists from A to C does not mean the direct edge
A → Cexists. They are different questions and, as we'll see, with very different computational costs. - Assuming every directed graph is a DAG. The absence of cycles must be checked, not assumed; real data (entered by users) contains accidental cycles more often than you'd imagine.
- Forgetting isolated vertices. A task with no dependencies at all (incoming or outgoing) is still a vertex of the graph. The sets
VandEare independent: there can be vertices with no edges. - Tip: before writing any code, draw the graph (mermaid or paper). With graphs, a good drawing prevents half of all modeling mistakes.
Exercises
Exercise 1: classifying graphs
For each system, say whether its natural graph is directed or undirected, weighted or not, and whether you expect it to be a DAG: (a) the "derived from" history between versions of a document; (b) commercial flights between airports with their duration; (c) power sockets connected by cables in an office.
Exercise 2: reading a dependency graph
Using the course convention (A → B = B depends on A) and this lesson's TaskFlow DAG, answer: (a) what are the in-degree and out-degree of integration_tests? (b) which tasks can start on day one (without waiting for anyone)? (c) write a path from design_schema to launch and give its length.
Exercise 3: causing a cycle
Starting from the same DAG, add a single edge that creates a cycle, and explain — using the "depends on" reading — why the resulting project is impossible to execute.
Solutions
Solution 1:
- (a) Directed (the "derived from" relation has a direction), unweighted, and it is a DAG: a version can't derive from a future version of itself.
- (b) Directed (outbound and return may differ or not exist), weighted (duration), and it is not a DAG: cycles (Madrid → Paris → Madrid) not only exist but are desirable.
- (c) Undirected (a cable connects both ways), weighted if we care about cable length; "DAG" doesn't apply to undirected graphs.
Solution 2:
- (a) In-degree 2 (
deploy_apiandimplement_uipoint at it), out-degree 1 (launch). - (b) The ones with in-degree 0:
design_schema,configure_server, anddesign_ui. - (c)
design_schema → migrate_db → deploy_api → integration_tests → launch, length 4 (four edges, five vertices).
Solution 3: For example launch → design_schema. Reading: "design_schema depends on launch". But launch depends (transitively) on design_schema, so each waits for the other through the chain: no task in the cycle ever reaches in-degree 0 and the entire project is blocked. Any "backward" edge over an existing path counts as a valid answer.
Conclusion
We now speak the language of graphs: vertices and edges; directed, weighted, degrees, paths, cycles, components, dense versus sparse, and the DAG as the faithful portrait of a well-formed project. We've placed lists and trees as graphs with restrictions, and we've fixed the convention that will govern the whole module: the arrow points at the task that gets unblocked. What we don't know yet is how to store a graph in memory: one big "who connects to whom" table, or a dictionary of neighbors? The answer — adjacency matrix versus adjacency list, with their costs and a reusable Graph class — is exactly the topic of the next lesson.
Data Structures Course
Module 1: Introduction to Data Structures
- What Are Data Structures?
- The Importance of Data Structures in Programming
- Types of Data Structures
- Algorithmic Complexity and Big O Notation
- Arrays and Memory: the Foundation of Data Structures
Module 2: Lists
Module 3: Stacks
- Introduction to Stacks
- Basic Stack Operations
- Stack Implementation
- Stack Applications
- Stack Exercises
Module 4: Queues
- Introduction to Queues
- Basic Queue Operations
- Circular Queues
- Priority Queues
- Double-Ended Queues (Deques)
- Queue Exercises
Module 5: Hash Tables and Dictionaries
- Introduction to Hash Tables
- Hash Functions and Collision Resolution
- Dictionaries and Sets in Practice
- Hash Table Exercises
Module 6: Trees
- Introduction to Trees
- Binary Trees
- Tree Traversals
- Binary Search Trees
- AVL Trees
- B-Trees
- Heaps
- Tree Exercises
Module 7: Graphs
- Introduction to Graphs
- Graph Representation
- Graph Search Algorithms
- Shortest Path Algorithms
- Minimum Spanning Trees
- Graph Applications
- Graph Exercises
