Week 11
VL 20 - 23.06.25
- revision of previous lecture: definition of a heap
for the heap tree and als its subtrees: root has the highest priority (max heap) \(\Rightarrow\) parent has higher priority than its children
the heap tree is perfectly balanced & left-leaning \(\Rightarrow\) flattening as Array is possible
for any node k
- parent: (k - 1) // 2
- left child: 2 * k + 1
- right child 2 * k + 2
insert operation:
- append new elements at the end of the array (O(1))
- whenever necessary repair the heap-condition with
upheap()
after inserting
implementation
class Heap: def __init__(self): self.data = [] def push(self, priority): self.data.apipend(priority) self.data, len(self.data) - 1) upheap( def upheap(a, k): while True: if k == 0: # repair has ended break = (k - 1) // 2 parent if a[parent] > a[k]: # heap-cond holds break = a[k], a[parent] # swap a[parent], a[k] = parent k
Heap Data Structure (Cont)
- deleting an element:
- deleting the largest element (the first element):
- replace last element (the smallest) with the first element (the largest) and delete the last element. (now the first element is violating the heap cond because it is the smallest)
- repair the heap condition starting from the root, successively pushing node down
number of comparisons O(d) = O(logN)def pop(self): = len(self.data) - 1 last self.data[0] = self[last] del self.data[last] self.data, last - 1) downheap( def downheap(a, last): = 0 # k while True: = 2 * k + 1, 2 * k + 2 left, right if left > last: # repair has ended, because k is a leaf break if right <= last and a[right] > a[left]: = right child else: = left child if a[k] > a[child]: break # heap cond holds = a[child], a[k] # swap a[k], a[child] = child k
Heapsort - Sorting with the Heap Techniques
the idea is to first create a heap from the array
implementation:
def heap_sort(a):
= len(a)
N for k in range(1, N):
upheap(a, k)# a is sorted a a sa heap
for k in range(N - 1, 0, - 1): # loop iteration backwards
0], a[k] = a[k], a[0] # bring the currently largest element to the position k
a[- 1) # repair the heap condition in the remaining heap downheap(a, k
Treap Data Structure
Treap is a simultaneously both
- a search tree
- a heap
implementation:
class TreapNode:
def __init__(self, key, priority, value):
self.key = key
self.value = value
self.priority = priority
self.left = self.right = None
- idea:
- The tree satisfies the search tree condition w.r.t the key
- The tree satisfies the heap condition w.r.t. the priority
- The inventor of the Treap DS showed that it is possible and feasible to satisfies both conditions at the same time
- e.g. insert:
- normal
tree_insert
w.r.t thekey
(priority is ignored) \(\Rightarrow\) search tree condition is satisfied - repair the heap condition on the way back of the recursive call stack, if the current node has lower priority than its child (important: the heap-condition can be only violated w.r.t to one of the children, namely in the subtree in which it was inserted)
- if the left child has higher priority \(\Rightarrow\) right-rotation
- if the right child has higher priority \(\Rightarrow\) left-rotation
- normal
- e.g. insert:
- possible appropriate priorities:
- random numbers \(\Rightarrow\) the tree is balanced on average
- access counter \(\Rightarrow\) rotate the element upwards if more often access than the parent (access optimized tree, i.e. important elements are closer to the root - and this is faster.)
Index Sort / Indirect Sort
given:
a
- an unsorted arrayp
- array, where the indices are stored in a sorted order.p[i] -> k
(index). k is the index of an element before the sorting, s.t.i
is the index after the sorting. (p
is a permutation of the numbers 0, …, N - 1)applications:
- iff a read-only, in-place is not possible
- if multiple arrays have to be sorted in the same way (?)
implementation
def index_sort(a, p): = [None] * len(a) r for i in range(len(a)): = a[p[i]] r[i] return r
p
can be obtained by any sorting algorithm, where the key-functoin accesses the original Array= [...] # read-only array, to be sorted a = list(range(len(a))) # indices 0, ..., N - 1 p = lambda k: a[k]) quick_sort(p, key_function = index_sort(a, p) r
VL 21 - 26.06.25
Graphs & Graph Theory
Origins of graph theory: Solution to the Kongisberg Problem:
- is it possible to make a city tour without crossing any bridge ever twice?
Euler’s answer: No.
Solution: An Euler path exist only if all nodes (other than start and end nodes) have an even number edges incident to them (i.e. the degree of the nodes is even)
Definitions for Graphs:
- Graph G = (V, E)
- V: set of all nodes / vertices
- E: Set of all edges, \(E \subseteq V \times V\)
- V: set of all nodes / vertices
- This is a normal graph: at most one edge between any two nodes. Initially we also exclude self loops, i.e. \((u, u) \notin E\)
- Multigraph: multiple edges between nodes are allowed (Like in the Konigsberg example)
- undirected graph: \((v, u) \in E \Rightarrow (u, v) \in E\)
- directed graph: not undirected.
- Degree of a node:
- undirected: number of edges incident to a node (we don’t count twice)
- directed:
- out degree: number of outgoing edges
- in degree: numb of inocming edges
- Complete Graph:
Let \(N := |V|, M := |E|\). For a complete graph we have:
\(degree(u) = N - 1\)
handshaking lemma: \(M = \frac{N(N - 1)}{2}\)
Subgraph: \(G' = (V', E'), V' \subseteq V, E' \subseteq E\) and \((u, v) \in E' \Rightarrow V'\)
- induced subgraph: for an arbitrary \(V' \subseteq V\), \(E' := E \\ \{(u, v) : u \notin V' \text{or} v \notin V'\}\)
- spanning subtree: \(V' = V, E' \subset E\)
Walks (Weg) in graphs:
- trivial walk of length 0: individual node
- walk of length 1: a single edge of a graph
- a generalized walk of a graph: a sequence of nodes \((v_1, v_2, ... v_k)\), s.t. \((v_i, v_{i + 1}) \in E, \forall i\) \(\Rightarrow\) length:= \(k - 1\)
Path: A Walk where each individual node is distinct (other than possibly start and end)
Cycle (Zyklus): a Walk with \(v_1 = v_k\)
Circuit (sometimes Cycle) (Kreis): a path with \(v_1 = v_k\)
Reachability (Erreichbarkeit): \(u \rightsquigarrow v \Leftrightarrow u \rightsquigarrow v :\Leftrightarrow\) there is a Walk between the nodes \(u, v\)
Euler walk: each edge is contained exactly once. (example: Haus of Niklaus)
Hamilton path: each node is contained eactly once
Hamilton Circuit: A Hamilton path, s.t. start = end.
- This definition is relevant to the problem of the “Traveling Salesman”: find the hamilton circuit with the least weight (for graphs with weighted edges). Best known algorithm is \(\mathcal{O}(2^N)\).
- This is the standard example of a “NP-hard problem” \(\Rightarrow\) no efficient algorithm is known.
- weighted graphs:
- node-weighted: a number (weight) is assigned to every node
- edge-weighted: a number (weight) is assigned to every edge
- or both
- example: a graph of streets: edge weights = length of the roads
- Planar Graphs:
- a planar graph can be drawn in 2D, s.t. no edges intersect.
- ex.: Haus of Niklaus
- euler’s formula for any planar graph drawn without crossings: \(V - E + F = 2\)
- Dual Graphs:
- each surface gets a node
- each edge defines a border between surfaces - each surface seperated by an edge is connected with an edge
Euler’s Formula (Primal Form)
For any connected planar graph drawn without edge crossings:
\[ V - E + F = 2 \]
Where:
- \(V\) = number of vertices
- \(E\) = number of edges
- \(F\) = number of faces, including the outer (infinite) face
Dual Graphs and Euler’s Formula
The dual graph \(G^*\) of a planar graph \(G\) is constructed by:
- Placing a vertex in each face of \(G\)
- Connecting two vertices in \(G^*\) if their corresponding faces in \(G\) are adjacent (i.e., separated by an edge in \(G\))
In the dual:
- The number of vertices in \(G^*\) is equal to the number of faces in \(G\): \(V^* = F\)
- The number of edges remains the same: \(E^* = E\)
- The number of faces in \(G^*\) equals the number of vertices in \(G\): \(F^* = V\)
Euler’s Formula Applied to the Dual
Since the dual \(G^*\) is also planar and connected (if \(G\) is), Euler’s formula also holds:
\[ V^* - E^* + F^* = 2 \]
Substituting the dual identities:
\[ F - E + V = 2 \]
This shows that Euler’s formula is preserved under duality, with \(V \leftrightarrow F\), and \(E\) unchanged.
Summary Table
Property | Original Graph \(G\) | Dual Graph \(G^*\) |
---|---|---|
Vertices | \(V\) | \(V^* = F\) |
Edges | \(E\) | \(E^* = E\) |
Faces | \(F\) | \(F^* = V\) |
Euler’s Formula | \(V - E + F = 2\) | \(V^* - E^* + F^* = 2\) |
Connected Graph
any two nodes \(u, v\) are connected \(u \rightsquigarrow v\).
- connected components: maximal subgraphs of the graph that are connected.
- tree: connected graph, s.t. no cucles exist
- for a tree: \(|E| = |V| - 1\)
- forest: a not connected graph, s.t. each connected component is a tree
- spanning tree: a subgraph of a connected graph that is a tree, s.t. all nodes are touched.
- \(\Rightarrow\) theorem: for a connected graph such a spanning tree always exists.