Have you ever wondered how the Google Maps find the fastest way or how to see how to see Netflix? The graphs behind these decisions are the algorithm.
Made of graphs, nodes (points) and edges (contacts), computer is one of the most powerful data structures in science. They effectively help model relationships from social networks to transportation systems.
In this guide, we will find two basic trousers techniques: the first search of width (BFS) and deeper search (DFS). Moving from there, we will cover modern algorithms such as Digestra, A*, Crisis, Prime, and Bell Manford.
Table of contents:
Understanding the graph in azagar
Contains a graph Nodes (vertical) And Edges (relationships).
For example, in a social network, people are nodes and friendship edges. Or in a roadmap, city nodes and roads are on the edges.
There are some different types of graph:
Instruction: There is direction in the edges (one -way roads, task scheduling).
Non -instruction: Both of the shores walk in both ways (mutual friendship).
Weight: Values ​​in the edges (distance, costs).
Restless: The edges are equal (basic subway paths).
Now that you know what the graphs are, let’s look at the different ways they can be represented.
Ways to represent the graph in azagar
Before diving into a traversal and path -finding, it is important to know how the graph can be represented. Different issues demand different representatives.
Adjacent matrix
Adjacent matrix is ​​a 2D array where each cell (i, j) Shows an edge from the node i To node j.
One in Non -weight graphFor, for, for,.
0This means no edge, and1That means there is one edge.A Weights graphWeighs on the seal edge.
This makes it very quick to find out if the two nodes are directly attached (looking for permanent time), but it uses more memory for large graphs.
graph = (
(0, 1, 1),
(1, 0, 1),
(1, 1, 0)
)
Here, the matrix shows a fully connected graph of 3 nodes. For example, graph(0)(1) = 1 This means that the node is one edge from 0 to Node 1.
Affiliate List
An adjacent list represents each node with a list of nodes associated with it.
This is usually more effective for the viral graph (where not every node is connected to every other node). This saves memory because only the original edges are preserved instead of the entire grid.
graph = {
'A': ('B','C'),
'B': ('A','C'),
'C': ('A','B')
}
Here, node A Connects from B And CAnd so on. Checking contacts takes a little longer than the matrix, but big, viral graphs IT, this is a better option.
Using Network X
When working on real -world applications, writing your affiliate lists and matrix can hurt. There Network X It comes, a Azgar library that facilitates the creation and analysis of the graph.
With just a few lines of the code, you can make graphs, imagine them, and run modern algorithms without re -applying the wheel.
import networkx as nx
import matplotlib.pyplot as plt
G = nx.Graph()
G.add_edges_from((('A','B'), ('A','C'), ('B','C')))
nx.draw(G, with_labels=True)
plt.show()
This produces a triangle -shaped graph with Nodes A, B, and C, Network X allows you to run the algorithm easily, such as the shortest paths or trees without manually codes them.
Now when we have seen different ways to represent the graph, let’s move towards troubling methods, starting with the first search (BFS).
First Search of Width (BFS)
The basic idea behind the BFS is to detect a layer at a time. He sees all the neighbors of the early node before going to the next level. A queue is used to keep the coming items track.
BFS is particularly useful for this:
Here is an example:
from collections import deque
def bfs(graph, start):
visited = {start}
queue = deque((start))
while queue:
node = queue.popleft()
print(node, end=" ")
for neighbor in graph(node):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
graph = {
'A': ('B','C'),
'B': ('A','D','E'),
'C': ('A','F'),
'D': ('B'),
'E': ('B','F'),
'F': ('C','E')
}
bfs(graph, 'A')
What is happening in this code is here:
graphThere is a duct where each node maps the list of neighbors.dequeFIFO is used as a row so we go to the level of nodes levels.visitedTracking the nodes that we have already taken action so we do not have a loop on bicycles forever.In the loop, we pop a node, print it, then for every unhealthy neighbor, we visit it and invoke it.
And here is outpat:
A B C D E F
Now that we have seen how the BFS works, let’s turn to its counterpart: the first search for depth (DFS).
First search for depth (DFS)
The DFS does different work from the BFS. Instead of moving the surface in terms of surface, it goes on a path as far as back tracking. Think about diving deep under the trail, then come back to find others.
We can enforce DFS in two ways:
Recursive dfsWho uses the function call steak
TAKARY DFSWho uses a clear stack
DFS is particularly useful for this:
The cycle detection
Maze solutions and puzzles
Topologic sorting has been
Here is an example of DFS:
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
if node not in visited:
print(node, end=" ")
visited.add(node)
for neighbor in graph(node):
dfs_recursive(graph, neighbor, visited)
graph = {
'A': ('B','C'),
'B': ('A','D','E'),
'C': ('A','F'),
'D': ('B'),
'E': ('B','F'),
'F': ('C','E')
}
dfs_recursive(graph, 'A')
visitedThere is a set that already tracks processing nodes so you don’t have a loop on bicycles forever.On each call, if
nodeNot seen, it is hidden, marked, then the function runs again in every neighbor.
Traversal order:
A B D E F C
Explanation: DFSB visits A, goes deeper in D, then tracks E and F search backback, and eventually visits C.
And here is an example of DFS:
def dfs_iterative(graph, start):
visited = set()
stack = (start)
while stack:
node = stack.pop()
if node not in visited:
print(node, end=" ")
visited.add(node)
stack.extend(reversed(graph(node)))
dfs_iterative(graph, 'A')
visitedThe tracks nodes have already taken action so that you do not loop bicycles.stackLifo (in the last, is the first out) – youpop()The above node, take action on it, then press your neighbors.reversed(graph(node))Pushes neighbors into reverse so they actually go to left to right order (routinely imitating DFS).
Output it is:
A B D E F C
With the explanation of BFS and DFS, we can now move towards the algorithm that solves more complex problems, which begin with the shortest route to the Digaccer.
The algorithm of the Digxistra
The algorithm of the Dhakstra is made on a simple principle: Always go to the node with the lowest known distance. By repeating it, it is naked from the lowest way to the node that begins in the weight graph, with no negative edges.
import heapq
def dijkstra(graph, start):
heap = ((0, start))
shortest_path = {node: float('inf') for node in graph}
shortest_path(start) = 0
while heap:
cost, node = heapq.heappop(heap)
for neighbor, weight in graph(node):
new_cost = cost + weight
if new_cost < shortest_path(neighbor):
shortest_path(neighbor) = new_cost
heapq.heappush(heap, (new_cost, neighbor))
return shortest_path
graph = {
'A': (('B',1), ('C',4)),
'B': (('A',1), ('C',2), ('D',5)),
'C': (('A',4), ('B',2), ('D',1)),
'D': (('B',5), ('C',1))
}
print(dijkstra(graph, 'A'))
What is happening in this code is here:
graphIs an affiliate list: Each node in the map list(neighbor, weight)Coupleshortest_pathStores the most famous distance for each node (initially, 0 forstart,heap(Priority Row) was the Frontier Nodes to(cost, node)Always pop the smallest price.For every pop
nodeIt gives comfort to your edges: for each(neighbor, weight)Computersnew_cost. Unlessnew_costHeartbeatshortest_path(neighbor)Update it and press the neighbor at this price.
And here is outpat:
{'A': 0, 'B': 1, 'C': 3, 'D': 4}
Going forward, let’s see the extension of this algorithm: A search.<
A* Search
A* acts like a dodkistra but adds a hoverstick function that estimates how close the node is to purpose. It makes it more efficient by guiding the search in the right direction.
import heapq
def heuristic(node, goal):
heuristics = {'A': 4, 'B': 2, 'C': 1, 'D': 0}
return heuristics.get(node, 0)
def a_star(graph, start, goal):
g_costs = {node: float('inf') for node in graph}
g_costs(start) = 0
came_from = {}
heap = ((heuristic(start, goal), start))
while heap:
f, node = heapq.heappop(heap)
if f > g_costs(node) + heuristic(node, goal):
continue
if node == goal:
path = (node)
while node in came_from:
node = came_from(node)
path.append(node)
return path(::-1), g_costs(path(0))
for neighbor, weight in graph(node):
new_g = g_costs(node) + weight
if new_g < g_costs(neighbor):
g_costs(neighbor) = new_g
came_from(neighbor) = node
heapq.heappush(heap, (new_g + heuristic(neighbor, goal), neighbor))
return None, float('inf')
graph = {
'A': (('B',1), ('C',4)),
'B': (('A',1), ('C',2), ('D',5)),
'C': (('A',4), ('B',2), ('D',1)),
'D': ()
}
print(a_star(graph, 'A', 'D'))
It’s a bit more complicated, so what’s going on here:
graph: Adjacent list – each node map((neighbor, weight), ...).heuristic(node, goal): A estimate lootedh(node)(Less better). It has passedgoalBut the demo uses a fixed duct.g_costs: The most famous price fromstartOn each node (∞ initially, 0 for start).heap: At least(priority, node)Inpriority = g + h.came_from: Once we have back points to re -form the path to pop a goal.
Then in the main loop:
We pop the node with a small priority.
If this is aimed at, we turn back behind it
came_fromTo make the way and return withg_costs(goal).Otherwise, we relax the edges: for each
(neighbor, weight)Computersnew_cost = g_costs(node) + weight. Unlessnew_costBrings improvementg_costs(neighbor)Update it, set itcame_from(neighbor) = nodeAnd push(new_cost + heuristic(neighbor, goal), neighbor).
Output:
(('A', 'B', 'C', 'D'), 4)
Next, let’s go to the trees spread through the shortest paths. This is the place where the algorithm of Kerskal comes.
Kissle’s algorithm
The algorithm of Kreskal makes the minimum stretch tree (MST), which makes all the edges sorting to the smallest of the largest and adding one of them at a time, unless they make a cycle. This makes it a greedy algorithm because it always chooses the cheapest option available at every step.
Implementation uses an unwanted set (union-turned) data structure to effectively examine whether to add an edge or not. Each node starts in its seat, and as the edges are added, the sets are available.
class DisjointSet:
def __init__(self, nodes):
self.parent = {node: node for node in nodes}
self.rank = {node: 0 for node in nodes}
def find(self, node):
if self.parent(node) != node:
self.parent(node) = self.find(self.parent(node))
return self.parent(node)
def union(self, node1, node2):
r1, r2 = self.find(node1), self.find(node2)
if r1 != r2:
if self.rank(r1) > self.rank(r2):
self.parent(r2) = r1
else:
self.parent(r1) = r2
if self.rank(r1) == self.rank(r2):
self.rank(r2) += 1
def kruskal(graph):
edges = sorted(graph, key=lambda x: x(2))
mst, ds = (), DisjointSet({u for e in graph for u in e(:2)})
for u,v,w in edges:
if ds.find(u) != ds.find(v):
ds.union(u,v)
mst.append((u,v,w))
return mst
graph = (('A','B',1), ('A','C',4), ('B','C',2), ('B','D',5), ('C','D',1))
print(kruskal(graph))
Output:
(('A','B',1), ('C','D',1), ('B','C',2))
Here, the smallest edges have been added to the MST that connect all the nodes without making a bicycle. Now that we’ve seen Kreskal, we can move further to analyze another algorithm.
The algorithm of prime
The prime algorithm also gets an MST, but it grows step by step. It starts a node and repeated Adds the smallest edge Which connects the existing tree to a new node. Think about expanding it in the “island” associated with all the nodes.
This implementation A Priority Row (Hep Q) The smallest available edge always to select effectively.
import heapq
def prim(graph, start):
mst, visited = (), {start}
edges = ((w, start, n) for n,w in graph(start))
heapq.heapify(edges)
while edges:
w,u,v = heapq.heappop(edges)
if v not in visited:
visited.add(v)
mst.append((u,v,w))
for n,w in graph(v):
if n not in visited:
heapq.heappush(edges, (w,v,n))
return mst
graph = {
'A':(('B',1),('C',4)),
'B':(('A',1),('C',2),('D',5)),
'C':(('A',4),('B',2),('D',1)),
'D':(('B',5),('C',1))
}
print(prim(graph,'A'))
Output:
(('A','B',1), ('B','C',2), ('C','D',1))
Consider how the algorithm gradually spreads from the node AAlways choose the lowest weight edge that connects a new node.
Now let’s look at an algorithm that can handle the graph along the negative edges: Bellman Ford.
Bellman Ford algorithm
Bell Manford is the shortest route algorithm that can handle the negative edge weight unlike the degex. It works Repeatedly resting all the edges: If the current route to the node can be improved through another node, it updates the distance. After V-1 Repetition (where V The number of verticals), all the shortest routes are guaranteed.
This makes it a degexist but a little more versatile. It can also detect negative weight cycles by examining further improvement after the central loop.
def bellman_ford(graph, start):
dist = {node: float('inf') for node in graph}
dist(start) = 0
for _ in range(len(graph)-1):
for u in graph:
for v,w in graph(u):
if dist(u) + w < dist(v):
dist(v) = dist(u) + w
return dist
graph = {
'A':(('B',4),('C',2)),
'B':(('C',-1),('D',2)),
'C':(('D',3)),
'D':()
}
print(bellman_ford(graph,'A'))
Output:
{'A': 0, 'B': 4, 'C': 2, 'D': 5}
Here, the shortest path to each node is found, though it contains negative edges (B → C With weight -1). If there was a negative cycle, Bellman Ford would find out that the distances would continue after that. V-1 Repetition
With the explanation of the important algorithm, let’s move on to some practical points to make their implementation more efficient.
Improve the graph algorithm in Uzar
When the graph grows, how do you write your code is a bit compatible. There are some easy but powerful tricks here to run things easily.
1. Usage deque For BFS
If you use a regular list as a row, it takes more time in the list of popped items from the front. With collections.dequeYou get quick (O(1)) Pop from both ends. It is mainly built for such a job.
from collections import deque
queue = deque((start))
2. Repeat with DFS
The repetitive DFS looks clear, but does not like to go too deep – if your graph is huge, you will target the repetition limit. OK? Write the DFS in a stupid manner with the stack. The same idea, no repetition mistakes.
def dfs_iterative(graph, start):
visited, stack = set(), (start)
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
stack.extend(graph(node))
3. Let the network X do heavy lifting
You have to write your own graph code for exercise and learning. But if you are working on a real-world issue-say that analyzing social networks or planning routes-the network X-library saves tons of time. It comes with a better version of excellent visual tools in addition to almost every common graph algorithm.
import networkx as nx
G = nx.Graph()
G.add_edges_from((('A','B'), ('A','C'), ('B','D'), ('C','D')))
print(nx.shortest_path(G, source='A', target='D'))
Output:
('A', 'B', 'D')
Instead of worrying about rows and piles, you can focus on what the Network X handle and the results mean.
Key path
The adjacent matrix search is faster but the memory is heavy.
The adjoining list is effective Passient Space of the Viral Graph.
Network X makes graph analysis much easier for real -world projects.
The BFS detects the layer through the layer, DFS discovers deeper before tracking.
Digaccers and a* shortest way.
Blood trees of Christian and prime.
Bell Manford works with negative weight.
Conclusion
Graphs from maps to social networks are present everywhere, and the algorithm you have seen here are building blocks to work with them. Whether it is looking for the way, the construction of the spread of trees, or dealt with difficult weight, these tools open up to you widely solving problems.
When you are ready to take major projects, keep and try libraries like Network X.