Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Graph Structure & Decomposition  ›  Capstone Project

Capstone: Meridian's Network Model

50 minPieces, backbone and weak points in one report
You're building a piece ofMeridian — the network model
This piece — network_report(): One report on the whole road network: its pieces, its backbone, and its weak points.
Scenario Meridian's planners want one page about the road network before anything gets routed over it: is the city one piece, what would the cheapest backbone cost, and which roads and junctions are single points of failure. The network report ties the section's tools into that page.
Your task
Build network_report(junctions, roads). Each road is [a, b, cost]: two-way, all costs different, no road from a junction to itself and at most one road per pair. Return a dict with: "pieces", the number of separate pieces the network falls into; "backbone", the total cost of the cheapest set of roads that keeps every piece connected; "critical_roads", every road whose closure splits a piece, as [a, b] with a before b, sorted; and "critical_junctions", every junction whose closure splits a piece, sorted.

Capstone: Meridian's Network Model

In short: one sorted union-find pass answers two questions — how many pieces the network falls into, and what the cheapest backbone linking each piece costs — because Kruskal's algorithm is that pass, just with the roads sorted first. One low-link DFS over the same roads then answers two more: every critical road (a bridge) and every critical junction (an articulation point). Four numbers, one sort, one union-find pass and one DFS.

Two pieces: a triangle a, b, c with roads a-b (2) and b-c (1) kept for the backbone and a-c (3) rejected as a ring, plus a separate pair x-y (5) that is the whole second piece and its one critical road

Meridian's planners want one page about the road network before anything gets routed over it. Is the city one piece, or are there parts no van can reach? What would the cheapest backbone cost, the fibre that keeps every depot talking? And where are the weak points, the roads and junctions whose closure would split the city?

Every answer is something you've already built in this section. The capstone puts them together into one function, network_report, and shows that one union-find pass and one depth-first search are enough for all four numbers.

Part 1: pieces and the backbone, in one pass

A road network can be in several pieces: a new estate whose access road isn't finished, or an island reached only by ferry. Counting pieces is union-find: start with one piece per junction, and every road that joins two different groups leaves one fewer.

What does this print?
junctions = ["a", "b", "c", "d", "e", "f"]
roads = [("a", "b"), ("c", "d"), ("d", "e"), ("e", "c")]
parent = {j: j for j in junctions}

def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

pieces = len(junctions)
for a, b in roads:
    ra, rb = find(a), find(b)
    if ra != rb:
        parent[ra] = rb
        pieces -= 1
print(pieces)

Now look at what that loop is doing. It's Kruskal's algorithm without the sort. Sort the roads by cost first and the same unions build the cheapest backbone. On a network in several pieces, Kruskal's never finds a road between them and simply returns one tree per piece, a minimum spanning forest. So one pass gives two answers: pieces is junctions minus successful unions, and the backbone cost is the sum of their costs.

The backbone isn't always the cheap roads, though:

What does this print?
roads = [("a", "b", 1), ("b", "c", 2), ("c", "a", 3), ("c", "d", 100)]
parent = {j: j for j in "abcd"}

def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

total = 0
for a, b, cost in sorted(roads, key=lambda r: r[2]):
    ra, rb = find(a), find(b)
    if ra != rb:
        parent[ra] = rb
        total += cost
print(total)

That road to d is a critical road, and the second prediction is no accident.

Every critical road is in the backbone

A critical road is the only link between its two sides, so every spanning tree has to include it, the cheapest one included. Here is the city with its backbone drawn, then its critical roads marked. Watch where they land:

watch it move · graph

The cheapest backbone has no spare links, so every critical road in the city is one of its links.

Eight junctions and nine roads. Each number is the cost of laying fibre along that road. 8 nodes, 9 edges, undirected. At central.

1 / 5

For Meridian that's a planning lesson, not only a theorem. The cheapest backbone has no redundancy at all, so any road the city can't afford to lose, the fibre can't afford to lose either. Resilience costs extra links beyond the tree.

Part 2: weak points, in one DFS

Critical roads and critical junctions come out of the same low-link DFS. When a child finishes, fold its low-link into its parent's, then check:

  • low[child] > disc[parent]: the road parent–child is critical;
  • low[child] >= disc[parent], for a parent that isn't the DFS root: the parent junction is critical;
  • the DFS root is critical only if it had two or more DFS children.

That third rule is easy to forget, and forgetting it marks every root:

broken — fix it

Only c is critical (closing it strands d), but this also reports a, which is the DFS root. Fix the root's rule.

Run the DFS from every undiscovered junction, and a network in several pieces is covered too. Each piece gets its own root and its own low-links.

Trace the union-find half on a small network with an island:

step through it
1junctions = ["a", "b", "c", "x", "y"]
2roads = [["a", "b", 2], ["b", "c", 1], ["c", "a", 3], ["x", "y", 5]]
3parent = {j: j for j in junctions}
4 
5def find(x):
6 while parent[x] != x:
7 x = parent[x]
8 return x
9 
10pieces, cost = len(junctions), 0
11for a, b, c in sorted(roads, key=lambda r: r[2]):
12 ra, rb = find(a), find(b)
13 if ra != rb:
14 parent[ra] = rb
15 pieces -= 1
16 cost += c
17print(pieces, cost)

The report, drawn from your own code

Below, your code runs on a city extract with a ferry-only island. It builds the backbone road by road, then marks the critical roads in red and badges each junction with its piece. Try it: add ["east", "yard", 11] and every critical road on the mainland disappears. Join the island instead with ["dock", "pier", 12], and the piece count drops while two new critical roads appear.

your code, drawn · graph

Your network report. The backbone grows cheapest first, then one DFS marks every weak point.

Putting it together

Two helpers and a dict:

python

The weak-points helper is the iterative low-link DFS from the bridges lesson, with the root-child count added. Here's what the whole report costs:

Part Tool Cost
pieces union-find nearly O(E)
backbone Kruskal's (the same unions, sorted) O(E log E)
critical roads low-link DFS O(V + E)
critical junctions the same DFS free
whole report O(E log E)

The sort is the most expensive step. Everything structural about the city comes from one sort, one union-find pass and one DFS. Space is O(V + E): near holds every road, parent one entry per junction, and disc/low/the DFS stack together hold at most one entry per junction too.

❓ Cross-question: "Why not reuse the backbone to find the critical roads? They're all in it." Every critical road is in the backbone, but not every backbone link is critical. On a ring, the backbone is all but one road and none of them is critical, because the skipped road is the way round. Being critical depends on the roads the backbone left out, so it needs the full network.


Idioms & real-world patterns

One report, many consumers

This is how production network tools are built. A planning service computes components, spanning structure and single points of failure once, whenever the map changes, and every other system (routing, dispatch, alerting) reads the result rather than recomputing it. Keeping the helpers separate, as above, lets each part be tested against the slow definitions from its own lesson.

What comes next for Meridian

With the network modelled, Meridian can start moving things through it. That means shortest routes that survive negative adjustments, all-pairs distances between depots, and heuristic search fast enough for a live map. Those build on this section: routing on a DAG uses the topological order from the first lesson, and every route stays inside one piece of the network.

⚡ Advanced: keeping it current

Roads open and close all day. Recomputing the whole report is O(E log E), which is fine every few minutes for a city. Keeping the answers exact after every single change is a research-grade problem (dynamic connectivity). Most real systems batch the changes and recompute.


Quick practice

What does this print?
junctions = ["m", "n", "o", "p"]
roads = [["m", "n", 3], ["n", "o", 1], ["o", "m", 4]]
parent = {j: j for j in junctions}

def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

pieces, cost = len(junctions), 0
for a, b, c in sorted(roads, key=lambda r: r[2]):
    ra, rb = find(a), find(b)
    if ra != rb:
        parent[ra] = rb
        pieces, cost = pieces - 1, cost + c
print(pieces, cost)
broken — fix it

The triangle's cheapest backbone costs 1 + 2 = 3, but this prints a total of 6 because it keeps a-b (5) before it ever sees the cheaper roads. Fix the loop so it visits roads in cost order.

🎯 Your turn

Write network_report(junctions, roads). Roads are two-way [a, b, cost], all costs different. Return:

  • "pieces": how many separate pieces the network falls into;
  • "backbone": the total cost of the cheapest roads keeping every piece connected;
  • "critical_roads": every road whose closure splits a piece, as [a, b] with a before b, sorted;
  • "critical_junctions": every junction whose closure splits a piece, sorted.

network_report(["a", "b", "c"], [["a", "b", 2], ["b", "c", 3]]) → {"pieces": 1, "backbone": 5, "critical_roads": [["a", "b"], ["b", "c"]], "critical_junctions": ["b"]}

Hint: one sorted union-find pass for the first two, one low-link DFS per piece for the last two, and don't forget the root rule.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = Meridian's network model is complete. ✅

Practice — level up

0/6 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upHow many pieces?

Write count_pieces(junctions, roads) (roads are [a, b, cost]) returning how many separate pieces the road network falls into. Example: ["a", "b", "c"], [["a", "b", 4]] → 2.

connected-componentsunion-find
DrillA backbone for every piece

Write forest_cost(junctions, roads) returning the total cost of the cheapest set of roads that keeps each piece of the network connected. That's a minimum spanning forest: one tree per piece. Example: ["a", "b", "c", "x", "y"], [["a", "b", 1], ["b", "c", 2], ["c", "a", 3], ["x", "y", 5]] → 8.

spanning-forestkruskal
BuildCould every road be one-way?

Meridian is considering making every road one-way to ease congestion. Write can_go_one_way(junctions, roads) (roads here are [a, b]) returning True if some choice of directions still lets every junction reach every other. Example: a triangle → True; a straight line of three → False.

bridgesccconnected-components
BossChoose the directions

Now produce the plan. Write one_way_plan(junctions, roads) (roads [a, b]) returning a direction for every road, as a sorted list of [from, to], such that every junction still reaches every other, or None if that's impossible. Build it with DFS from the alphabetically smallest junction, visiting neighbours in sorted order: a road that discovers a new junction points away from the root, and every other road points back up to the older junction. A single junction with no roads → []. Example: triangle a, b, c → [["a", "b"], ["b", "c"], ["c", "a"]].

dfs-treebridgescc
CapstoneOne new road to end all closures

The network is connected. Contractors have quoted for new roads, each [a, b, cost] between junctions not already joined by a road. Write cheapest_fix(junctions, roads, quotes) (existing roads are [a, b]) returning the cost of the cheapest single new road after which no road is critical. Return 0 if no road is critical already, and -1 if no single quote does it. Example: line a–b–c–d with quotes a–c at 2, a–d at 7 and b–d at 3 → 7.

bridgelow-linkminimum-spanning-trees
ExplainWhy every critical road is in the backbone

Check the city report: every critical road also appears in the backbone, whatever the costs are. Explain why a critical road must be in every spanning tree of its piece, and what that means for Meridian's fibre and road planning.

network_report.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
network_report(junctions, roads) → dictPieces, backbone cost, critical roads and critical junctions of the road network.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.