How to detect a cycle in a dictionary? - python

I have the following dictionary with the first number being the node, followed by a tuple consisting of a neighbor and then the edge weight (some nodes have multiple neighbors and weights):
dictionary = {1: [(2, 3), (4, 5), (6, 2)], 2: [(3, -4)], 3: [(8, 4)], 4: [(5, 6)], 5: [(4, -3), (8, 8)], 6: [(7, 3)], 7: [(6, -6), (8, 7)]}
How would I use Depth First Search (DFS) to detect if there is a cycle or not? The dictionary will be changed frequently for testing purposes, so some may have cycles and some may not. I am new to python and do not understand how to implement DFS to check for a cycle or not. I have the following DFS code:
visited = set() # Set to keep track of visited nodes of graph.
def dfs(visited, dictionary, node): #function for dfs
if node not in visited:
print (node)
visited.add(node)
for neighbour in dictionary[node]:
dfs(visited, dictionary, neighbour)
and then I call the function: dfs(visited, dictionary, '1').
But I get the error KeyError: '1'. I also am unsure how to use this code to detect for a cycle or not.

First we consider the base cases. In this instance, we wish to evaluate whether the graph has a cycle or not. The function should return true when a cycle is detected, and false if no such cycle exists after all edges have been examined.
dictionary = {1: [(2, 3)], 2: [], 3: [(4, 4)], 4: [(3, 6)]}
def DFS(visited, dictionary, node):
if node in visited:
return True
else:
for neighbour, weight in dictionary[node]:
if DFS(visited | {node}, dictionary, neighbour):
return True
return False
print(f"{DFS(set(), dictionary, 1)=}") # False
Depth first search will examine all paths from a single node, going exploring as far as it can before backtracking. If it encounters a node it has seen before, the search will stop and the function returns true. If all paths from the starting node have been exhausted, it returns false. This will only detect whether node 1 is involved in any cycles.
However, this approach is not enough to detect cycles which does not involve the starting node. In the above example, the DFS will not find the cycle between nodes 3 and 4, as it starts its search from 1.
To fix this, we can write another function which examines all the nodes with the DFS.
def containsCycle(dictionary):
for node in dictionary.keys():
if DFS(set(), dictionary, node):
return True
return False
print(f"{containsCycle(dictionary)=}") # True
dictionary = {1: [(2, 3), (4, 5), (6, 2)], 2: [(3, -4)],
3: [(8, 4)], 4: [(5, 6)], 5: [(4, -3), (8, 8)],
6: [(7, 3)], 7: [(6, -6), (8, 7)]}
The dictionary above has a edges to a node 8, without an entry for that node in the dictionary. This will cause an error. To resolve this, we can add a guard clause to our recursive function.
def DFS(visited, dictionary, node):
if node in visited:
return True
elif node not in dictionary: # Guard clause
return False
else:
for neighbour, weight in dictionary[node]:
if DFS(visited | {node}, dictionary, neighbour):
return True
return False

Related

while condition true if value appears in list of tuples

I'm new to Python and got stuck while implementing an algorithm for drawing graphs. I have a list of tuples that contain certain nodes and their 'level' (for layering). Initially that looks like this:
[(0, 10), (1, 'empty'), (2, 'empty'), (3, 'empty'), (4, 'empty'), (5, 'empty'), (6, 'empty'), (7, 'empty'), (8, 'empty'), (9, 'empty')] (node,level)
Now I need to assign levels to these nodes as long as any node has no level, respective the 'empty' attribute.
I tried several structures of possible while conditions, that syntactically weren't wrong but semantically didn't make any sense, since the while loop didn't terminate:
while (True for node, level in g.nodes(data='level') if level == 'empty'):
or
while ( 'empty' in enumerate(g.nodes(data='level') ):
and certain other similar constructs, that didn't work and I don't remember..
Until now it doesn't seem clear to me why this won't work - python didn't even enter the while loop with these conditions. Can you explain me why and hand me a clue how to fix it?
hint:
g.nodes(data='level') is a networkx function, that returns the upper list of tuples
So the things you have in the parentheses are generator expressions and they just always evaluate to True.
You have the right idea but maybe need to learn a bit more about these expressions and what to do with them. In your first suggestions, here is what I _ think_ you mean:
while any(level == 'empty' for node, level in g.nodes(data='level')):
...
In your version, what you do is create a generator expression that will container a number of True values, one for each level that is empty. However, even if that generator expression will have no elements in it, it is in and of itself not an empty object, and thus it evaluates to True.
You can try this out:
bool([]) # --> False because empty sequence
bool(list(range(1, -10))) # --> False because empty sequence
bool((i for i in range(1,-10))) # --> True because non-None generator expression
So you need to turn your generator expression into a truth value that actually reflects whether it has any true elements in it, and that's what the any function does: Take an iterator (or a generator expression) and return true if any of its elements are true.
You're trying to make an elemental check do your searching for you. Instead, concentrate on extracting exactly the values you need to check. Let's start with the list of tuples as
g = [(0, 10), (1, 'empty'), (2, 'empty'), (3, 'empty'), (4, 'empty'),
(5, 'empty'), (6, 'empty'), (7, 'empty'), (8, 'empty'), (9, 'empty')]
Now get a list of only the levels:
level = [node[1] for node in g]
Now, your check is simply
while "empty" in level:
# Assign levels as appropriate
# Repeat the check for "empty"
level = [node[1] for node in g]
If you're consistently updating the main graph, g, then fold the level extraction into the while:
while "empty" in [node[1] for node in g]:
# Assign levels
Originally, your while failed because you return True so long as there's anything in g:
while (True for node, level in ...)
You have a good approach, but made it one level too complex, and got stuck with True.
Your enumerate attempt fails because you search for a string in an enumerator, rather than in its returned values. You could make a list of the returned values, but then we're back where we started, with a list of tuples. This would compare "empty" against each tuple, and fail to find the desired string -- it's one level farther down.
The other answers may be best. But the specific test you requested can be written a few ways:
This is similar to what you had, but creates a list instead of a generator. The list will evaluate as False if it is empty, but the generator doesn't.
while [True for node, level in g.nodes(data='level') if level == 'empty']:
...
This is an equally effective version. The list just has to be empty or not; it doesn't matter whether the elements are True or False or something else:
while [node for node, level in g.nodes(data='level') if level == 'empty']:
...
Or you can use Python's any function, which was made for this and will be a little more efficient (it stops checking elements after the first match). This can use a generator or a list.
while any(level == 'empty' for node, level in g.nodes(data='level')):
...
It is often more Pythonic just to use a for loop:
nodes = [(0, 10),
(1, 'empty'),
(2, 'empty'),
(3, 'empty'),
(4, 'empty'),
(5, 'empty'),
(6, 'empty'),
(7, 'empty'),
(8, 'empty'),
(9, 'empty')]
for node in nodes:
if node[1]=="empty":
node[1] = set_level_func() # customise this function as you wish
If you're just trying to assign levels for each if one is empty then a simple for loop should suffice:
data = [(0, 10), (1, 'empty'), (2, 'empty'), (3, 'empty'), (4, 'empty'), (5, 'empty'), (6, 'empty'), (7, 'empty'), (8, 'empty'), (9, 'empty')]
output = []
for node, level in data:
if level == 'empty':
level = ? #question mark is whatever level logic you wish to insert
output.append((node, level))

Building a collection across recursive iterations

I am attempting to implement an "all paths" algorithm described in this question's top answer using Python.
So far, I have defined the function as such:
def AllPaths(start, end, edges):
print(start)
if (start == end):
return
for i in range(0, len(edges)):
if edges[i][1] == start:
AllPaths(edges[i][0], end, edges)
I have included print(start) to try give myself an idea of the function's behaviour.
For example, if the initial function call is as such:
edges = [ (1, 2), (1, 3), (2, 3), (3, 4) ]
AllPaths(edges[len(edges) - 1][1], edges[0][0], edges)
With the starting node being 4 and the ending node being 1, the output of the function is:
4
3
1
2
1
Somewhere in that output, it is telling me that all paths between node 4 and node 1 consists of:
[ (4, 3), (3, 1) ]
[ (4, 3), (3, 2), (2, 1) ]
But how do I form these collections during execution and, if I wanted to keep track of how many distinct paths there are, how would this be counted during execution?
If you don't mind that I won't follow the backward traversal route, which I find rather odd, then here's a suggestion on how to modify your function to see what's going on:
def all_paths(start, end, edges):
if start == end:
return [[end]]
paths = []
for edge in edges:
if edge[0] == start:
paths += [[start] + path
for path in all_paths(edge[1], end, edges)]
return paths
Output for edges = [(1, 2), (1, 3), (2, 3), (3, 4)] and start = 1; end = 4:
[[1, 2, 3, 4],
[1, 3, 4]]
If you want a direct edges-result then I'd suggest:
def all_paths(start, end, edges):
paths = []
for edge in edges:
if edge[0] == start:
if edge[1] == end:
return [[edge]]
else:
paths += [[edge] + path
for path in all_paths(edge[1], end, edges)]
return paths
Output for the same input:
[[(1, 2), (2, 3), (3, 4)],
[(1, 3), (3, 4)]]
But a warning: If you have loops in the graph, e.g. edges = [(1, 2), (1, 3), (3, 1), (2, 3), (3, 4)], then these algorithms will crash (recursion won't stop). In these cases something like
def all_paths(start, end, edges):
edges = set(edges)
paths = []
for edge in edges:
if edge[0] == start:
if edge[1] == end:
paths += [[start, end]]
else:
new_edges = edges.difference({edge})
paths += [[start] + path
for path in all_paths(edge[1], end, new_edges)]
return paths
which removes visited edges is better. Result for start = 1; end = 4:
[[1, 2, 3, 1, 3, 4],
[1, 2, 3, 4],
[1, 3, 1, 2, 3, 4],
[1, 3, 4]]
I hope that this is at least a little bit what you were looking for.

find path and convert from dictionary to list

I am using networkx library for Python with BFS and DFS. I need to get a tree and then explore it to get a path from a start node to an end node.
For the BFS part I am using bfs_successorsand it returns an iterator of successors in breadth-first-search from source.
For the DFS part I am using: dfs_successors and it returns a dictionary of successors in depth-first-search from source.
I need to get a list of nodes from source to end from both the algorithms. Each node is (x, y) and is a cell in a grid.
Here's what I've done so far:
BFS = nx.bfs_successors(mazePRIM, start)
print(dict(BFS))
DFS = nx.dfs_successors(mazePRIM, start)
print(DFS)
and I get this:
{(0, 0): [(0, 1), (1, 0)], (1, 0): [(1, 1)], (1, 1): [(1, 2)], (1, 2): [(0, 2), (1, 3)], (0, 2): [(0, 3)]}
{(0, 0): [(0, 1), (1, 0)], (1, 0): [(1, 1)], (1, 1): [(1, 2)], (1, 2): [(0, 2), (1, 3)], (0, 2): [(0, 3)]}
but I need an output like this:
[(0, 0), (1, 0), (1, 1), (1, 2), (1, 3)]
which is the list of nodes from start to end.
Do you have any advice about how to do it? Can you help me please?
Use a list comprehension and imply add .keys() to the end of your dictionaries:
DFS = nx.bfs_successors(mazePRIM,start)
print([n for n in dict(BFS).keys()])
DFS = nx.dfs_successors(mazePRIM, start)
print([n for n in DFS.keys()])
You can read more about dictionary keys here:
How to return dictionary keys as a list in Python?
You can simply convert the dictionary keys directly to a list:
DFS = nx.bfs_successors(mazePRIM,start)
print(list(dict(BFS).keys()))
DFS = nx.dfs_successors(mazePRIM, start)
print(list(dict(DFS).keys()))

Sorting a list of tuples made up of two numbers by consecutive numbers

The list of tuples is the output from a capacitated vehicle-routing optimization and represents the arcs from one stop to another, where 0 represents the depot of the vehicle (start and end point of vehicle). As the vehicle must drive several laps it may return to the depot before all stops were made. The solver will always return the starting-arcs first, which in the example below means that the first consecutive tuples starting with 0, namely (0, 3), (0, 7), (0, 8) will determine how many laps (= 3) there are.
How can I sort the arcs in consecutive order so that one vehicle could drive the arcs one after another?
Input:
li = [(0, 3), (0, 7), (0, 8), (3, 0), (4, 0), (7, 3), (8, 4), (3, 0)]
Output:
[(0, 3), (3, 0), (0, 7), (7, 3), (3, 0), (0, 8), (8, 4), (4, 0)]
What I tried so far:
laps = 0
for arc in li:
if arc[0] == 0:
laps = laps + 1
new_list = []
for i in range(laps):
value = li.pop(0)
new_list.append([value])
for i in range(laps):
while new_list[i][-1][1] != 0:
arc_end = new_list[i][-1][1]
for j in range(len(li)):
if li[j][0] == arc_end:
value = li.pop(j)
new_list[i].append(value)
break
flat_list = [item for sublist in new_list for item in sublist]
return flat_list
You are trying to solve the problem of finding cycles in a directed graph. The problem itself is not a difficult one to solve, and Python has a very good package for solving such problems - networkx. It would be a good idea to learn a bit about fundamental graph algorithms. There is also another stack overflow question about this algorithm, which you can consult, at Finding all cycles in a directed graph

Recursive path counting

i am trying to count paths with a recursive function
if for example i have the given instructions
instructions = {
1: (('bot', 3), ('bot', 4)),
2: (('bot', 4), ('output', 0)),
3: (('output', 5), ('bot', 5)),
4: (('bot', 5), ('bot', 6)),
5: (('output', 1), ('bot', 7)),
6: (('bot', 7), ('output', 4)),
7: (('output', 2), ('output', 3))
}
`
represented by this picture
in this in example there are 3 paths from bot 5 (5-1, 5-7-2, 5-7-3). There are 6 paths from bot 4 (4-5-1, 4-5-7-2, 4-5-7-3, 4-6-7-2, 4-6-7-3, 4-6-4).
this is what i have tried so far but i have no success
def count_path(bot, instructions):
counter = 0
b = "bot"
outp = "output"
while True:
for x, y in instructions[bot]:
if x == b:
count_path(y, instructions)
elif x == outp:
counter += 1
return counter
For the sake of clarity, I changed the names from bot to node - representing a point that connects forward to other points, and output to end - representing a point that does not connect forward
instructions = {1: (('node', 3), ('node', 4)),
2: (('node', 4), ('end', 0)),
3: (('end', 5), ('node', 5)),
4: (('node', 5), ('node', 6)),
5: (('end', 1), ('node', 7)),
6: (('node', 7), ('end', 4)),
7: (('end', 2), ('end', 3))}
def count_path(current_node, instructions):
connections = instructions[current_node]
paths = 0
for connection in connections:
if connection[0] == "node":
paths += count_path(connection[1])
elif connection[0] == "end":
paths += 1
return paths
Basically, the recursion works by checking to see if points that connect to the current point are "node" or "end", if they are nodes, a new stack is created (ie. the function is called again) but starting from the nodes that connect to the new current node, it then checks if those points that connect to the new nodes are also nodes.
This continues until a "end" is reached. When this occurs it 1 is added to paths. The number of paths reached in the current stack are this returned to the stack above.
Recursion can be hard to visualise, but essentially as you reach end points and move back up the stacks, you are returning the total number of end points in that direction, and the top stack will therefore return the total number of end points (which is equivalent to the total number of paths).
You need to add the returned value from count_path and remove the while = true.
def count_path(bot, instructions):
counter = 0
b = "bot"
outp = "output"
for x, y in instructions[bot]:
if x == b:
counter += count_path(y, instructions)
else:
counter += 1
return counter
In Recursion you have a base case which in your case is when you meet a dead end i.e. an output element.
Lets suppose you are at bot a, if next element is bot b, then
no. path from bot a += no. of paths from bot b
Otherwise if the next element is a output, then add one because that is a termination of one of the paths you are counting.
This is a simple but working solution. However it only counts paths but not collect them.
instructions = {
1: (('bot', 3), ('bot', 4)),
2: (('bot', 4), ('output', 0)),
3: (('output', 5), ('bot', 5)),
4: (('bot', 5), ('bot', 6)),
5: (('output', 1), ('bot', 7)),
6: (('bot', 7), ('output', 4)),
7: (('output', 2), ('output', 3))
}
def count_paths(bot):
n_paths = 0
entry = instructions[int(bot)]
for path_type, bot_number in entry:
if path_type == 'output':
n_paths += 1
elif path_type == 'bot':
n_paths += count_paths(bot_number)
return n_paths
if __name__ == '__main__':
bot = input("Count path for: ")
paths = count_paths(bot)
print("For bot {bot} there are {paths} paths".format(bot=bot, paths=paths))

Categories

Resources