The Route with Fewer Stops Takes Longer
Distinguish minimum-hop paths from minimum-cost paths and implement Dijkstra’s nonnegative-cost frontier with explicit unreachable and tie semantics.
14 sections · 4 questions · 1 coding challenge
The arrow keys move between sections too.
Shortest Routes Needed a Cost-Aware Search
BOTIE
Dijkstra’s shortest-path method is used to find minimum accumulated cost in graphs with suitable nonnegative weights. Neo4j’s Graph Data Science documentation exposes that method separately from simple hop-count path matching. The algorithm keeps the cheapest unsettled distance visible rather than committing to the first route discovered.
For our cart, that distinction allows a route with an extra stop to win. The Python exercise implements a small nonnegative-weight version so we can inspect why the earlier expensive route is replaced.
Two Hops Are Slower Than Three
The maintenance cart can reach B directly from A in nine minutes. Going through C takes four minutes. A search that chooses the fewest relationships selects the direct route, even though the crew asked for the quickest one.
We need to define what “shortest” measures. Hop count treats every edge equally. Travel time assigns a cost to each edge, so the route choice must compare accumulated costs rather than the number of connections.
Invented maintenance records. Python exercises and workshop components expose the mechanism locally. Source-linked Cypher or SPARQL examples require the stated external database setup.
Step 1 of 2
Compare route costs rather than counting arrows
Each edge label is travel time in minutes.
Nodes and properties (4)
- A · Cart start
- B · Workshop
- C · Side corridor
- D · Destination
- A to B: 9 min
- A to C: 2 min
- C to B: 2 min
- B to D: 1 min
- C to D: 20 min
Step 2 of 2
Replace a tentative route when a cheaper one appears
The pending column shows the heap entries after each expansion, including entries that later become stale.
| Chosen node | Best known costs | Pending candidates |
|---|---|---|
| A at 0 | A 0; B 9; C 2; D unknown | C 2, B 9 |
| C at 2 | A 0; B 4; C 2; D 22 | B 4, B 9, D 22 |
| B at 4 | A 0; B 4; C 2; D 5 | D 5, B 9, D 22 |
| D at 5 | Destination cost settled at 5 | The stale B 9 entry cannot improve the answer |
A at 0
- Best known costs
- A 0; B 9; C 2; D unknown
- Pending candidates
- C 2, B 9
C at 2
- Best known costs
- A 0; B 4; C 2; D 22
- Pending candidates
- B 4, B 9, D 22
B at 4
- Best known costs
- A 0; B 4; C 2; D 5
- Pending candidates
- D 5, B 9, D 22
D at 5
- Best known costs
- Destination cost settled at 5
- Pending candidates
- The stale B 9 entry cannot improve the answer
Choose the route by travel time
Relaxing accumulated nonnegative costs can improve a previously discovered route. The final lookup distinguishes an unreachable destination from cost zero.
Choose the route by travel time
The fewest-hop shortcut loses to a longer route with lower accumulated minutes.
BOTIE
Result for these records
5
Follow the operations
1. Directed weighted route graph
Nodes and properties (4)
- A · Location
- B · Location
- C · Location
- D · Location
- A to B: ROUTE · minutes: 9
- A to C: ROUTE · minutes: 2
- C to B: ROUTE · minutes: 2
- B to D: ROUTE · minutes: 1
- C to D: ROUTE · minutes: 20
Inspect the node and relationship records
{ "nodes": [ { "id": "A", "kind": "Location" }, { "id": "B", "kind": "Location" }, { "id": "C", "kind": "Location" }, { "id": "D", "kind": "Location" } ], "edges": [ { "id": "ab", "source": "A", "target": "B", "type": "ROUTE", "minutes": 9 }, { "id": "ac", "source": "A", "target": "C", "type": "ROUTE", "minutes": 2 }, { "id": "cb", "source": "C", "target": "B", "type": "ROUTE", "minutes": 2 }, { "id": "bd", "source": "B", "target": "D", "type": "ROUTE", "minutes": 1 }, { "id": "cd", "source": "C", "target": "D", "type": "ROUTE", "minutes": 20 } ] }2. Read graph relationships
[ { "id": "ab", "source": "A", "target": "B", "type": "ROUTE", "minutes": 9 }, { "id": "ac", "source": "A", "target": "C", "type": "ROUTE", "minutes": 2 }, { "id": "cb", "source": "C", "target": "B", "type": "ROUTE", "minutes": 2 }, { "id": "bd", "source": "B", "target": "D", "type": "ROUTE", "minutes": 1 }, { "id": "cd", "source": "C", "target": "D", "type": "ROUTE", "minutes": 20 } ]3. Choose a relationship type
[ { "id": "ab", "source": "A", "target": "B", "type": "ROUTE", "minutes": 9 }, { "id": "ac", "source": "A", "target": "C", "type": "ROUTE", "minutes": 2 }, { "id": "cb", "source": "C", "target": "B", "type": "ROUTE", "minutes": 2 }, { "id": "bd", "source": "B", "target": "D", "type": "ROUTE", "minutes": 1 }, { "id": "cd", "source": "C", "target": "D", "type": "ROUTE", "minutes": 20 } ]4. Start location
A
5. Calculate nonnegative path costs
[ { "id": "A", "cost": 0 }, { "id": "B", "cost": 4 }, { "id": "C", "cost": 2 }, { "id": "D", "cost": 5 } ]6. Destination
D
7. Read one destination cost
5
8. Result
5
These components operate on the supplied records in your browser. They do not run a graph database or interpret Cypher.
1. Choose the quantity to minimise
Minimum hops answers how many links must be followed. Minimum travel time answers how much edge cost accumulates. Neither is inherently the correct question; the operational objective decides.
A directed route can be traversable in one direction only. If a corridor is bidirectional, represent both permitted directions or use a graph projection whose orientation is deliberately defined.
Our route graph is directed, and every minutes property uses the same unit.
2. Replace first discovery with the best known cost
B is first discovered with cost nine through the direct edge. When C is expanded, the route through C offers a lower cost to B. The algorithm updates B’s best value rather than treating its first discovery as final.
A priority queue makes the lowest current candidate the next one to process. Older queued entries for a node can become stale after an improvement, so the code checks them against the best-known distance.
The visited-on-discovery rule from unweighted BFS does not implement this weighted objective.
Compare the two routes to B
This isolates the accumulated-cost comparison before implementing a search.
direct=9
via_c=2+2
print({'direct':direct,'via_C':via_c}){'direct': 9, 'via_C': 4}The longer-in-hops route has the smaller time cost.
3. Use the heap to revisit a tentative choice
A Python heap stores the candidate with the smallest first tuple value at its front. heappush adds a candidate; heappop removes the cheapest one. The second tuple item is the node ID, which also provides a deterministic tie break for these string IDs.
We do not need to find and delete an old heap entry when a better route appears. Update best and push the cheaper candidate. When the old entry eventually comes out, compare its cost with best and skip it if they differ. The program below isolates that mechanism before the challenge adds graph traversal.
The heap remembers candidates. The best dictionary records the current best estimate for each node.
Replace B’s nine-minute candidate
Suppose A has already produced candidates C at two and B at nine. Expanding C finds a cheaper way to B.
import heapq
best={'B':9,'C':2}
pending=[(2,'C'),(9,'B')]
heapq.heapify(pending)
cost,node=heapq.heappop(pending)
proposed=cost+2 # C to B takes two minutes.
if proposed < best['B']:
best['B']=proposed
heapq.heappush(pending,(proposed,'B'))
while pending:
cost,node=heapq.heappop(pending)
if cost != best[node]:
print('skip',node,cost)
continue
print('use',node,cost)use B 4
skip B 9The old entry remains in the heap but cannot overwrite the improved estimate. This example teaches one relaxation, not the full shortest-path function.
4. Why nonnegative weights matter
Once the cheapest candidate is settled, nonnegative future edges cannot produce a cheaper route by leaving that settled frontier and later subtracting cost. That property supports Dijkstra’s decision rule.
Negative weights require another algorithm and careful treatment of negative cycles. A missing weight is not automatically zero. Validate the graph projection’s weight field and units before running the method.
The exercise accepts zero-cost edges but rejects negative or nonfinite costs.
Check yourself
Questions on Parts 1 to 4
Question 1 of 2
Which route has the lower accumulated cost?
Use the information below.
A→B costs 9; A→C costs 2; C→B costs 2.5. Do not mistake Cypher shortestPath for weighted optimisation
A hop-oriented shortestPath pattern does not automatically minimise a minutes property just because the relationship contains it. Weighted algorithms such as GDS Dijkstra use an explicitly projected weight.
For the tiny setup below, the reference query enumerates bounded paths and orders their summed cost. It exposes the objective in ordinary Cypher, but it is not a scalable replacement for a dedicated weighted algorithm.
This reference is limited to a tiny acyclic graph with all relevant paths within three hops.
Run in your own environment
Inspect accumulated route costs on the tiny graph
The small bounded enumeration is for explanation, not a production path planner.
In a fresh test database run CREATE (a:Location {id:'A'}),(b:Location {id:'B'}),(c:Location {id:'C'}),(d:Location {id:'D'}),(a)-[:ROUTE {minutes:9}]->(b),(a)-[:ROUTE {minutes:2}]->(c),(c)-[:ROUTE {minutes:2}]->(b),(b)-[:ROUTE {minutes:1}]->(d),(c)-[:ROUTE {minutes:20}]->(d).
Bind start A and target D. Use GDS Dijkstra with an appropriate projection for larger weighted problems.
MATCH p=(a:Location {id:$start})-[:ROUTE*1..3]->(b:Location {id:$target})
WITH p, reduce(total=0, r IN relationships(p) | total+r.minutes) AS cost
RETURN [n IN nodes(p) | n.id] AS route, cost
ORDER BY cost, route
LIMIT 1The route A,C,B,D has cost 5. The LIMIT does not prevent the preceding enumeration from doing substantial work on a different graph.
Official documentation. Documentation checked .
6. Keep unreachable and zero-cost answers distinct
If the destination cannot be reached, return no distance, represented by None in the exercise. If start equals target, the empty route has cost zero. A reachable route can also legitimately have zero total cost.
Do not replace absence with zero. That would make a disconnected workshop look as though the cart were already there.
The challenge returns the minimum cost only, so equally cheap paths do not require a route-selection tie break.
7. A computed route is only as current as its allowed edges
A closed corridor or a restricted area must be removed before routing. Applying permission checks only after choosing the cheapest path can select an unusable route and obscure valid alternatives.
Travel times may change while the cart moves. A graph snapshot provides an estimate under stated conditions, not a guarantee about the physical journey. Validate operational safety separately from minimising cost.
The workshop filters ROUTE edges and computes costs from the supplied snapshot.
Check yourself
Questions on Parts 1 to 7
Question 1 of 2
What should shortest_cost return under this contract?
Use the information below.
No directed route connects A to D.Try it with Botie
Build with Botie
Write and run Python