9.7 Weighted Graphs
You need to model relationships where connections have different costs.
9.7 Weighted Graphs
Problem
You need to model relationships where connections have different costs.
In many graph problems, not all edges are equal. A flight between two cities may take three hours while another takes ten. One network link may have high bandwidth while another is slow. One road may be short while another is long.
Treating every edge as identical loses information that is essential for finding optimal solutions.
You need a graph model that captures the cost, distance, time, capacity, risk, or value associated with each connection.
Solution
Associate a weight with every edge.
Instead of storing:
A → B
store:
A → B (5)
where 5 represents the weight.
The interpretation of the weight depends entirely on the problem domain.
Examples:
| Weight Meaning | Example |
|---|---|
| Distance | Kilometers between cities |
| Time | Minutes required for travel |
| Cost | Price of a flight |
| Risk | Probability of failure |
| Energy | Battery consumption |
| Capacity | Maximum throughput |
| Reliability | Connection quality |
A weighted adjacency list typically stores pairs:
graph = {
"A": [("B", 5), ("C", 2)],
"B": [("D", 7)],
"C": [("D", 1)],
"D": []
}
Each neighbor is accompanied by a weight.
Discussion
The introduction of weights changes the nature of many graph problems.
Consider:
A → B
A → C
C → D
B → D
In an unweighted graph, the shortest path is measured by edge count.
Both paths:
A → B → D
and
A → C → D
contain two edges.
They appear equally good.
Now assign weights:
A → B (10)
A → C (1)
C → D (1)
B → D (1)
Path costs become:
A → B → D = 11
A → C → D = 2
The second path is clearly better.
Once weights exist, shortest-path algorithms must optimize accumulated cost rather than edge count.
This distinction motivates algorithms such as Dijkstra, Bellman-Ford, and Floyd-Warshall.
Choosing Meaningful Weights
The weight must represent the quantity being optimized.
This sounds obvious, but many modeling mistakes begin here.
Suppose you are building a navigation system.
Possible weights include:
| Weight | Meaning |
|---|---|
| Distance | Shortest route |
| Time | Fastest route |
| Fuel | Most efficient route |
| Toll cost | Cheapest route |
| Accident risk | Safest route |
The graph structure remains identical.
Only the weight interpretation changes.
Different weights can produce completely different paths.
Always ask:
What exactly am I minimizing or maximizing?
before assigning edge weights.
Positive and Negative Weights
Many algorithms assume positive weights.
Example:
A → B (4)
B → C (3)
A → C (10)
All weights are positive.
This property allows Dijkstra's algorithm to work efficiently.
Some problems contain negative weights:
A → B (4)
B → C (-6)
A → C (5)
Negative weights often represent:
- Credits
- Discounts
- Profits
- Energy recovery
- Gains
Negative weights require additional care.
Some algorithms remain correct.
Others become invalid.
For example:
| Algorithm | Negative Weights |
|---|---|
| BFS | No |
| Dijkstra | No |
| Bellman-Ford | Yes |
| Floyd-Warshall | Yes |
| A* | Usually no |
Whenever negative weights appear, verify algorithm assumptions carefully.
Weighted Adjacency Lists
The most common representation stores (neighbor, weight) pairs.
graph = {
0: [(1, 5), (2, 2)],
1: [(3, 4)],
2: [(3, 1)],
3: []
}
Traversal becomes:
for neighbor, weight in graph[0]:
print(neighbor, weight)
Output:
1 5
2 2
This structure works naturally with shortest-path and spanning-tree algorithms.
Recipe: Building a Weighted Graph
Suppose the input contains:
edges = [
("A", "B", 5),
("A", "C", 2),
("B", "D", 7),
]
Build an adjacency list:
def build_graph(edges):
graph = {}
for source, target, weight in edges:
graph.setdefault(source, []).append(
(target, weight)
)
graph.setdefault(target, [])
return graph
Result:
{
"A": [("B", 5), ("C", 2)],
"B": [("D", 7)],
"C": [],
"D": []
}
This representation supports efficient traversal and weight access.
Computing Path Costs
The cost of a path is the sum of its edge weights.
Given:
A → B (4)
B → C (3)
C → D (2)
Path:
A → B → C → D
has total cost:
4 + 3 + 2 = 9
A simple helper function:
def path_cost(path, weights):
total = 0
for i in range(len(path) - 1):
total += weights[(path[i], path[i + 1])]
return total
Example:
weights = {
("A", "B"): 4,
("B", "C"): 3,
("C", "D"): 2,
}
print(path_cost(
["A", "B", "C", "D"],
weights
))
Output:
9
This additive property is the foundation of most shortest-path algorithms.
Weight Matrices
Some algorithms operate more naturally on matrices.
For example:
inf = float("inf")
matrix = [
[0, 5, 2, inf],
[inf, 0, inf, 7],
[inf, inf, 0, 1],
[inf, inf, inf, 0]
]
Entry:
matrix[u][v]
stores the weight of edge u → v.
Missing edges use infinity.
This representation is common in:
- Floyd-Warshall
- Dynamic programming approaches
- Dense graphs
- Matrix-based graph computations
For sparse graphs, adjacency lists are usually preferable.
Weight Types
Weights need not be integers.
Floating-point values are common.
("A", "B", 3.14)
Probabilities:
("A", "B", 0.95)
Costs:
("A", "B", 12.99)
Durations:
("A", "B", 4.5)
The algorithm usually treats weights as numeric values supporting comparison and addition.
Choose the type that matches the domain.
Recipe: Convert Unweighted to Weighted
Some algorithms expect weights even when the graph is logically unweighted.
Assign every edge a weight of one.
graph = {
"A": ["B", "C"],
"B": ["D"],
"C": ["D"],
"D": []
}
Convert:
weighted = {
vertex: [
(neighbor, 1)
for neighbor in neighbors
]
for vertex, neighbors
in graph.items()
}
Result:
{
"A": [("B", 1), ("C", 1)],
"B": [("D", 1)],
"C": [("D", 1)],
"D": []
}
Many shortest-path algorithms then operate without modification.
Weighted Trees
Weights frequently appear in trees.
Example:
A
/ \\n (4) (2)
B C
/ \\n (1) (6)
D E
Applications include:
- Network design
- Routing
- Hierarchical clustering
- File-system metrics
- Geographic distances
The weight of a tree path is the sum of edge weights encountered during traversal.
Many tree algorithms generalize naturally once weights are introduced.
Complexity Characteristics
Weighted graphs have the same storage complexity as their unweighted counterparts.
For adjacency lists:
| Operation | Complexity |
|---|---|
| Add edge | O(1) amortized |
| Traverse neighbors | O(degree) |
| DFS | O(V + E) |
| BFS traversal | O(V + E) |
| Memory usage | O(V + E) |
The difference is algorithmic.
Finding a shortest path in an unweighted graph often uses BFS.
Finding a shortest path in a weighted graph may require:
- Dijkstra
- Bellman-Ford
- Floyd-Warshall
- A*
The complexity depends on the algorithm, not merely the representation.
Common Pitfalls
Do not confuse edge count with path cost.
The path with fewer edges is not necessarily cheaper.
Do not use Dijkstra when negative weights are possible.
Do not choose arbitrary weights without a clear optimization objective.
Do not use floating-point equality comparisons when precision matters.
Do not store costs in integer types that may overflow.
Do not forget that weight interpretation is part of the model.
Changing the meaning of weights changes the problem being solved.
Recipe: Validate Weights
Many graph systems benefit from validating weights during graph construction.
def validate(edges):
for source, target, weight in edges:
if weight is None:
raise ValueError(
"Missing weight"
)
Additional checks may enforce:
- Positive weights
- Nonnegative weights
- Integer weights
- Capacity constraints
- Probability ranges
Validation catches modeling errors before algorithm execution.
Takeaway
Weighted graphs extend ordinary graph models by associating a numeric value with each edge. These weights represent quantities such as distance, time, cost, risk, energy, or capacity and fundamentally change how optimal paths are computed. Understanding weighted graph representations, path costs, positive and negative weights, and weight selection is essential because many of the most important graph algorithms are designed specifically to reason about edge weights rather than simple connectivity.