Skip to content

Commit b958cd4

Browse files
committed
graph-bfs-dfs python renamed from .py to .md
graph-bfs 2/2
1 parent ca86189 commit b958cd4

File tree

2 files changed

+307
-263
lines changed

2 files changed

+307
-263
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
---
2+
jupytext:
3+
encoding: '# -*- coding: utf-8 -*-'
4+
text_representation:
5+
extension: .md
6+
format_name: myst
7+
kernelspec:
8+
display_name: Python 3 (ipykernel)
9+
language: python
10+
name: python3
11+
language_info:
12+
name: python
13+
nbconvert_exporter: python
14+
pygments_lexer: ipython3
15+
---
16+
17+
# graph browsing
18+
19+
+++ {"tags": ["prune-cell"]}
20+
21+
````{admonition} nothing to prune
22+
:class: warning
23+
24+
there are no difference - apart from this very cell - between the teacher and the student version, but the notebook is duplicated in .teacher for consistency
25+
````
26+
27+
+++
28+
29+
## depth-first or breadth-first scanning
30+
31+
given a non-valued directed graph G, and a start vertex V, there are 2 famous algorithm to walk the graph from V
32+
33+
* depth-first (DF) browsing, and
34+
* breadth-first (BF) browsing
35+
36+
intuitively, considering for example from the following tree
37+
38+
```{code-cell} ipython3
39+
import graphviz
40+
41+
tree = graphviz.Digraph(engine='dot')
42+
tree.edge('v', 'v1')
43+
tree.edge('v1', 'v11')
44+
tree.edge('v1', 'v12')
45+
tree.edge('v11', 'v111')
46+
tree.edge('v11', 'v112')
47+
tree.edge('v12', 'v121')
48+
tree.edge('v12', 'v122')
49+
tree.edge('v', 'v2')
50+
tree.edge('v2', 'v21')
51+
tree.edge('v2', 'v22')
52+
tree.edge('v21', 'v211')
53+
tree.edge('v21', 'v212')
54+
tree.edge('v22', 'v221')
55+
tree.edge('v22', 'v222')
56+
57+
tree
58+
```
59+
60+
these 2 algorithms would yield the nodes in the following orders
61+
62+
+++ {"cell_style": "split"}
63+
64+
DF browsing **from `v`** would scan
65+
```
66+
v v1 v11 v111 v112
67+
v12 v121 v122
68+
v2 v21 v211 v212
69+
v22 v221 v222
70+
```
71+
72+
+++ {"cell_style": "split"}
73+
74+
BF browsing **from `v`** would scan
75+
```
76+
v
77+
v1 v2
78+
v11 v12 v21 v22
79+
v111 v112 v121 v122 v211 v212 v221 v222
80+
```
81+
82+
+++
83+
84+
## objectives
85+
86+
+++
87+
88+
we want to write a **generator** that implements these 2 browsing policies from a graph and vertex.
89+
90+
of course, only the nodes reachable from the entry vertex will be browsed by this method
91+
92+
+++
93+
94+
## algorithms
95+
96+
the 2 algorithms used to perform these scans are, interestingly, **very close** to one another
97+
98+
in both cases we need a STORAGE object, where we can `store()` things and `retrieve()` them later on
99+
100+
+++
101+
102+
### FIFO / FILO
103+
104+
+++
105+
106+
Let us consider the following 2 storage implementations:
107+
108+
* `Fifo` implements a *first-in-first-out* policy
109+
* `Filo` does the exact opposite and has a *first-in-last-out* policy
110+
111+
Remember the regular `list` class is more optimized for a `append()/pop()` usage
112+
113+
So to work around that, we're using a `deque` class, instead of a simple list; it is actually useful only in the `Filo` case, but this way we have a more homogeneous code
114+
115+
**Exercise**: you may wish to factorize both into a single class...
116+
117+
But let's get to it:
118+
119+
```{code-cell} ipython3
120+
:cell_style: split
121+
122+
from collections import deque
123+
124+
class Fifo:
125+
def __init__(self):
126+
self.line = deque()
127+
def store(self, item):
128+
self.line.append(item)
129+
def retrieve(self):
130+
if self.line:
131+
return self.line.popleft()
132+
def __len__(self):
133+
return len(self.line)
134+
```
135+
136+
```{code-cell} ipython3
137+
:cell_style: split
138+
139+
from collections import deque
140+
141+
class Filo:
142+
def __init__(self):
143+
self.line = deque()
144+
def store(self, item):
145+
self.line.append(item)
146+
def retrieve(self):
147+
if self.line:
148+
return self.line.pop()
149+
def __len__(self):
150+
return len(self.line)
151+
```
152+
153+
```{code-cell} ipython3
154+
:cell_style: split
155+
156+
# let's check it does what we want
157+
158+
fifo = Fifo()
159+
for i in range(1, 4):
160+
fifo.store(i)
161+
while fifo:
162+
print(f"retrieve → {fifo.retrieve()}")
163+
```
164+
165+
```{code-cell} ipython3
166+
:cell_style: split
167+
168+
# same here
169+
170+
filo = Filo()
171+
for i in range(1, 4):
172+
filo.store(i)
173+
while filo:
174+
print(f"retrieve → {filo.retrieve()}")
175+
```
176+
177+
```{code-cell} ipython3
178+
def scan(start, storage):
179+
"""
180+
performs a scan of all the vertices reachable from start vertex
181+
182+
in an order that is DF or BF, depending on the
183+
storage policy (fifo or filo)
184+
185+
assumptions:
186+
187+
* vertices reachable from a vertex are
188+
stored in a 'neighbours' attribute
189+
190+
* storage should have store() and retrieve() methods
191+
and be testable for emptiness (if storage: ...)
192+
* also it should be empty when entering the scan
193+
"""
194+
195+
storage.store(start)
196+
# keep track of what we've seen
197+
scanned = set()
198+
199+
while storage:
200+
current = storage.retrieve()
201+
# skip vertices already seen
202+
if current in scanned:
203+
continue
204+
yield current
205+
scanned.add(current)
206+
for neighbour in current.neighbours:
207+
storage.store(neighbour)
208+
```
209+
210+
```{code-cell} ipython3
211+
class Vertex:
212+
def __init__(self, name):
213+
self.name = name
214+
self.neighbours = set()
215+
216+
def __repr__(self):
217+
return self.name
218+
219+
def add_neighbour(self, other):
220+
self.neighbours.add(other)
221+
```
222+
223+
```{code-cell} ipython3
224+
# rebuild sample graph
225+
def tree_vertex(name, depth):
226+
if depth == 0:
227+
return Vertex(name)
228+
elif depth > 0:
229+
result = Vertex(name)
230+
result.add_neighbour(tree_vertex(name+'1', depth-1))
231+
result.add_neighbour(tree_vertex(name+'2', depth-1))
232+
return result
233+
```
234+
235+
```{code-cell} ipython3
236+
g = tree_vertex('v', 3)
237+
g
238+
```
239+
240+
+++ {"cell_style": "split"}
241+
242+
### FILO = DF - depth first
243+
244+
+++ {"cell_style": "split"}
245+
246+
### FIFO = BF - breadth first
247+
248+
```{code-cell} ipython3
249+
:cell_style: split
250+
251+
for vertex in scan(g, Filo()):
252+
print(vertex)
253+
```
254+
255+
```{code-cell} ipython3
256+
:cell_style: split
257+
:inputHidden: false
258+
:outputHidden: false
259+
260+
for vertex in scan(g, Fifo()):
261+
print(vertex)
262+
```
263+
264+
### applications
265+
266+
+++
267+
268+
being a generator, we can combine it with all the `itertools` and the like
269+
270+
```{code-cell} ipython3
271+
import itertools
272+
```
273+
274+
for example, if we need to print every other vertex in a DF scan
275+
276+
```{code-cell} ipython3
277+
:cell_style: split
278+
279+
df_scan = scan(g, Filo())
280+
281+
for v in itertools.islice(df_scan, 0, None, 2):
282+
print(v)
283+
```
284+
285+
```{code-cell} ipython3
286+
:cell_style: split
287+
:tags: [level_intermediate]
288+
289+
# notice that df_scan is now exhausted !
290+
291+
for v in itertools.islice(df_scan, 0, None, 2):
292+
print(v)
293+
```
294+
295+
or skip the first 3..
296+
297+
```{code-cell} ipython3
298+
# but we can easily create a new one !
299+
df_scan = scan(g, Filo())
300+
301+
for v in itertools.islice(df_scan, 3, None):
302+
print(v)
303+
```
304+
305+
+++ {"cell_style": "center"}
306+
307+
***

0 commit comments

Comments
 (0)