+```python
+# https://dodona.be/nl/courses/3363/series/36083/activities/1049251771
+import copy
+from pathlib import Path
+from itertools import chain
+
+
+def longest_path_length(source: int, sink: int, graph_filename: str | Path) -> int:
+ """
+ Calculate the length of the longest path in the given graph between source and sink.
+
+ >>> longest_path_length(0, 4, 'data/04-data01.txt')
+ 9
+ """
+
+ graph = parse_graph(graph_filename)
+
+ path_lengths = [-1 for _ in range(max(len(graph), source, sink) + 1)]
+ path_lengths[source] = 0
+
+ for node in topological_ordering(graph):
+ if node is not source:
+ path_lengths[node] = max(path_lengths[predecessor] + weight for predecessor, weight in incoming_edges(graph, node))
+
+ return path_lengths[sink]
+
+
+def longest_path(source: int, sink: int, graph_filename: str | Path) -> tuple[int, ...]:
+ """
+ Calculate the longest path in the given graph between source and sink.
+
+ The path is constructed by using a backtracking algorithm.
+
+ >>> longest_path(0, 4, 'data/04-data01.txt')
+ (0, 2, 3, 4)
+ """
+
+ graph = parse_graph(graph_filename)
+
+ size = max(len(graph), source, sink) + 1
+
+ previous = [-1 for _ in range(size)]
+
+ path_lengths = [-1 for _ in range(size)]
+ path_lengths[source] = 0
+
+ # Calculate the path by weights
+ for node in topological_ordering(graph):
+ if node is not source:
+ # Calculate the longest path based on the incoming edges in the DAG
+ for predecessor, weight in incoming_edges(graph, node):
+ if path_lengths[node] < path_lengths[predecessor] + weight:
+ previous[node] = predecessor
+ path_lengths[node] = path_lengths[predecessor] + weight
+
+ # Reconstruct the path by backtracking
+ path = []
+ current = sink
+
+ while previous[current] >= 0:
+ path.append(current)
+ current = previous[current]
+
+ path.reverse()
+
+ return tuple(path)
+
+
+def parse_graph(graph_filename: str | Path) -> dict[int, list[tuple[int, int]]]:
+ """
+ Returns the list of edges in the given input file.
+
+ For every node, the list of outgoing edges and their weights are given.
+
+ >>> parse_graph('data/04-data01.txt')
+ {0: [(1, 7), (2, 4)], 1: [(4, 1)], 2: [(3, 2)], 3: [(4, 3)]}
+ """
+
+ # Graph of all nodes, with a list of outgoing edges
+ graph = {}
+
+ with open(graph_filename, 'r', encoding='utf-8') as graph_file:
+ for line in graph_file:
+ source, value = line.split('->')
+ source = int(source)
+ target, weight = map(int, value.split(':'))
+
+ if source not in graph:
+ graph[source] = []
+
+ graph[source].append((target, weight))
+
+ return graph
+
+def topological_ordering(graph: dict[int, list[tuple[int, int]]]) -> list[int]:
+ """
+ Returns a valid topological ordering of the given graph.
+
+ >>> topological_ordering({0: [(1, 7), (2, 4)], 1: [(4, 1)], 2: [(3, 2)], 3: [(4, 3)]})
+ [0, 1, 2, 3]
+ """
+
+ # List of nodes in topological order
+ node_order = []
+
+ # Set of nodes that still need to be visited.
+ remaining_nodes = set(graph)
+
+ while len(remaining_nodes) > 0:
+ # Select the next node.
+ source, value = min(graph.items(), key=lambda item: len(item[1]))
+
+ node_order.append(source)
+
+ # Remove all outgoing edges from remaining nodes.
+ for target, weight in graph[source]:
+ remaining_nodes.remove(target)
+
+ return node_order
+```