I am trying to solve a problem on Udacity described as follows:
# Find Eulerian Tour
#
# Write a function that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]
I came up with the following solution, which, while not as elegant as some of the recursive algorithms, does seem to work within my test case.
def find_eulerian_tour(graph):
tour = []
start_vertex = graph[0][0]
tour.append(start_vertex)
while len(graph) > 0:
current_vertex = tour[len(tour) - 1]
for edge in graph:
if current_vertex in edge:
if edge[0] == current_vertex:
current_vertex = edge[1]
elif edge[1] == current_vertex:
current_vertex = edge[0]
else:
# Edit to account for case no tour is possible
return False
graph.remove(edge)
tour.append(current_vertex)
break
return tour
graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)
>> [1, 2, 3, 1]
However, when submitting this, I get rejected by the grader. I am doing something wrong? I can't see any errors.
Here's a valid case where your algorithm fails:
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
Use the power of print
to find out what happens to graph
and current_vertex
.
Another hint: Move the else
down so that it belongs to the for
and is executed when the for
loop is not broken. As it is now, it can never be executed. After that correction, the algorithm still fails, of course.
The algorithm still fails, of course.
The algorithm still fails, of course.
Please, don't comment stating that the code doesn't work. It doesn't. The algorithm still fails, even if the code below does what the OP had in mind. The point was to show that the OP's algorithm is wrong, which the OP couldn't determine. For that, a correct implementation of OP's algorithm is needed (see below). A correct implementation of a wrong algorithm is still not a correct solution.
I'm sorry to make this answer worse by writing all these lengthy explanations, but people continue to complain that the code doesn't work (of course, the point was to show that it is wrong). They also downvote this answer, probably because they expect to be able to copy the code as a solution. But this is not the point, the point is to show to the OP that there is an error in his algorithm.
The code below does not find eulerian tours. Look elsewhere to copy code for passing your assingments!
def find_eulerian_tour(graph):
tour = []
current_vertex = graph[0][0]
tour.append(current_vertex)
while len(graph) > 0:
print(graph, current_vertex)
for edge in graph:
if current_vertex in edge:
if edge[0] == current_vertex:
current_vertex = edge[1]
else:
current_vertex = edge[0]
graph.remove(edge)
tour.append(current_vertex)
break
else:
# Edit to account for case no tour is possible
return False
return tour
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
print(find_eulerian_tour(graph))
Output:
[(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)] 1
[(2, 3), (3, 1), (3, 4), (4, 3)] 2
[(3, 1), (3, 4), (4, 3)] 3
[(3, 4), (4, 3)] 1
False
I'm also in the same lecture, WolframH's answer doesn't work for me. Here is my solution (has been accepted by the grader):
Push all possible next node
into a heap(search
) then search each one of them while recording.
def next_node(edge, current):
return edge[0] if current == edge[1] else edge[1]
def remove_edge(raw_list, discard):
return [item for item in raw_list if item != discard]
def find_eulerian_tour(graph):
search = [[[], graph[0][0], graph]]
while search:
path, node, unexplore = search.pop()
path += [node]
if not unexplore:
return path
for edge in unexplore:
if node in edge:
search += [[path, next_node(edge, node), remove_edge(unexplore, edge)]]
if __name__ == '__main__':
graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
print find_eulerian_tour(graph)
[1, 3, 4, 3, 2, 1]
Here's a case your algorithm can't handle: the complete graph on 4 vertices. Sticking a print tour
in there, you get:
>>> cg4 = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
>>> find_eulerian_tour(cg4)
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 0]
[0, 1, 2, 0, 3]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[etc.]
I'll leave you to find the problem with your approach -- you could easily google for a complete implementation, so since you didn't, I'm assuming you want the fun of figuring it out for yourself. :^)
Edit:
Hmmph. I admit I thought it was simply a missed failure case at the start. Anyway, @WolframH beat me to an updated example, but you could also look at the complete graph on 5 vertices, where your code gives
[0, 1, 2, 0, 3, 1, 4, 0]
and misses the edges (2,3), (2,4), and (3,4).
The question can be solved easily than the above solutions using simple recursion.
def find_eulerian_tour(graph):
tour=[]
find_tour(graph[0][0],graph,tour)
return tour
def find_tour(u,E,tour):
for (a,b) in E:
if a==u:
E.remove((a,b))
find_tour(b,E,tour)
elif b==u:
E.remove((a,b))
find_tour(a,E,tour)
tour.insert(0,u)
This code works for any input list of tuples and returns a list of the tour. Pls send suggestions and changes if any. Thanks @WolframH:Your code doesn't work if any loop exists in the graph and the tuples are entered just to fail your code.
I was going through the same course on Udacity. And I implemented Hierholzer's algorithm after reading it from Wikipedia. Here is the link to algorithm https://en.wikipedia.org/wiki/Eulerian_path
And below is my code. No doubt, it got accepted by the grader(after doing some Python3 to Python2 changes). :)
#!/usr/bin/env python3
# Find Eulerian Tour
#
# Write a program that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]
def get_a_tour():
'''This function returns a possible tour in the current graph and removes the edges included in that tour, from the graph.'''
global graph
nodes_degree = {} # Creating a {node: degree} dictionary for current graph.
for edge in graph:
a, b = edge[0], edge[1]
nodes_degree[a] = nodes_degree.get(a, 0) + 1
nodes_degree[b] = nodes_degree.get(b, 0) + 1
tour =[] # Finding a tour in the current graph.
loop = enumerate(nodes_degree)
while True:
try:
l = loop.__next__()
index = l[0]
node = l[1]
degree = nodes_degree[node]
try:
if (tour[-1], node) in graph or (node, tour[-1]) in graph:
tour.append(node)
try:
graph.remove((tour[-2], tour[-1]))
nodes_degree[tour[-1]] -= 1 # Updating degree of nodes in the graph, not required but for the sake of completeness.
nodes_degree[tour[-2]] -= 1 # Can also be used to check the correctness of program. In the end all degrees must zero.
except ValueError:
graph.remove((tour[-1], tour[-2]))
nodes_degree[tour[-1]] -= 1
nodes_degree[tour[-2]] -= 1
except IndexError:
tour.append(node)
except StopIteration:
loop = enumerate(nodes_degree)
if len(tour) > 2:
if tour[0] == tour[-1]:
return tour
def get_eulerian_tour():
'''This function returns a Eulerian Tour for the input graph.'''
global graph
tour = get_a_tour()
if graph: # If stuck at the beginning, finding additional tour in the graph.
loop = enumerate(tour[: -1])
l = loop.__next__()
i = l[0]
node = l[1]
try:
while True:
if node in list(zip(*graph))[0] or node in list(zip(*graph))[1]:
t = get_a_tour() # Retreivng the additional tour
j = t.index(node)
tour = tour[ : i] + t[j:-1] + t[ :j+1] + tour[i+1: ] # Joining the two tours.
if not graph: # Found Eulerian Tour
return tour # Returning the Eulerian Tour
loop = enumerate(tour[: -1]) # Still stuck? Looping back to search for another tour.
l = loop.__next__()
i = l[0]
node = l[1]
except StopIteration: # Oops! seems like the vertices in the current tour cannot connect to rest of the edges in the graph.
print("Your graph doesn't seem to be connected")
exit()
else: # Found the Eulerian Tour in the very first call. Lucky Enough!
return tour
# Sample inputs
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6)]
# graph = [(1, 2), (1, 3), (2, 3)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (9, 10), (10, 11), (11, 9)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (2, 7), (7, 8), (8, 2)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (1, 5), (5, 6), (1, 6)]
# graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
# graph = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
# graph = [(2, 6), (4, 2), (5, 4), (6, 5), (6, 8), (7, 9), (8, 7), (9, 6)]
# creating a {node: degree} dictionary
nodes_degree = {}
for edge in graph:
a, b = edge[0], edge[1]
nodes_degree[a] = nodes_degree.get(a, 0) + 1
nodes_degree[b] = nodes_degree.get(b, 0) + 1
#checking degree
degrees = nodes_degree.values() # remember it return a view
for degree in degrees:
if degree % 2:
print("Your graph have one or more nodes with odd degrees. Hence an Eulerian Tour is impossible.")
exit()
#finding Eulerian Tour
tour = get_eulerian_tour()
print(tour)
Hope this helps.
Though the code fails for Undirected graphs but runs perfectly well with directed ones. How ever it still doesn't solve the problem at hand from Udacity's side but can be treated as a lower version of the same. Please don't mind the badly used Python as I am still new to the language.
Two test scenarios of fairly good level of complexity added at the bottom.
initialNode = ''
nglength = 0
in_graphLength = 0
normalizedGraph = list()
path = []
node_dict = {}
mod_flag = ''
def find_eulerian_tour(graph):
global in_graphLength
in_graphLength = len(graph)
graph = normalize_graph(graph,[],-1,len(graph))
print (path)
return path
def normalize_graph(graph,nG,remNode,length):
counter = 0
global path
global initialNode
global in_graphLength
global nglength
global normalizedGraph
localGraph = list()
path = []
pathList = []
if(remNode != -1):
normalizedGraph = nG
baseNode = 0
if(len(normalizedGraph) != 0):
ini1, ini2 = normalizedGraph[0]
initialNode = ini1
a1,b1 = normalizedGraph[len(normalizedGraph) - 1]
baseNode = b1
if(remNode != -2):
graph.pop(remNode)
if(remNode == -1):
a,b = graph[0]
baseNode = b
normalizedGraph.append(graph[0])
initialNode = a
nglength = 1
graph.pop(0)
i = 0
if(len(graph) != 0):
for n1, n2 in graph:
i = i + 1
if(n1 == baseNode):
localGraph = graph[:]
if(isJunction((n1,n2),localGraph, nglength)):
graph.pop(i-1)
graph.append((n1,n2))
normalize_graph(graph, normalizedGraph, -2,in_graphLength)
break
else:
normalizedGraph.append((n1, n2))
nglength = nglength + 1
normalize_graph(graph, normalizedGraph, i - 1,in_graphLength)
break
else:
if( counter == 0):
counter = counter + 1
a0, b0 = normalizedGraph[0]
for n1, n2 in normalizedGraph:
path.append(n1)
path.append(a0)
path = path
return path
def isJunction((n1,n2), graph, nglength):
global node_dict
count = 0
if(len(graph) > 1):
for a1, a2 in graph:
if (n1 == a1):
count = count + 1
if (count > 1):
if(str(n1) not in node_dict):
key = str(n1)
node_dict[key] = count
else:
return handle_degree(n1)
return modification_needed((n1, n2), graph, nglength)
else:
return False
else:
return False
def handle_degree(n1):
global node_dict
key = str(n1)
if(node_dict.get(key) == 2):
return False
def modification_needed((n1,n2),graph, tmplength):
i = 0
global mod_flag
if( n2 == initialNode):
return True
if(len(graph) > 1):
for b1, b2 in graph:
if(n2 == b1):
i = i + 1
tmplength = tmplength + 1
if (b1,b2) in normalizedGraph:
mod_flag = True
continue
if(tmplength < in_graphLength and b2 == initialNode):
mod_flag = True
continue
else:
graph.pop(i-1)
modification_needed((b1,b2),graph,tmplength)
return mod_flag
#find_eulerian_tour([(1,2),(2,6),(7,2),(6,1),(2,3),(3,5),(3,4),(4,5),(5,7),(7,3),(5,6),(6,7)])
#find_eulerian_tour([(0,4),(1,0),(4,2),(4,8),(2,5),(9,5),(8,9),(5,4),(5,1),(7,1),(3,7),(1,6),(6,3)])
Here is the original code in Gregor Ulm's webpage and it works.
def find_eulerian_tour(graph):
def freqencies():
# save all nodes of edges to my_list
# e.g. [3,4,5,1,2,2,3,5]
my_list = [x for (x, y) in graph]
# get the max num of nodes-->create a list
# set all to 0
# for i in range(5) = 0 1 2 3 4
# so range("5" +1) means
# len=6, result=[0,0,0,0,0,0]
# so that the index = the number itself
result = [0 for i in range(max(my_list) + 1)]
# nodes in my_list, increment
# e.g. [0,1,2,2,1,2]
# 3appears 2times.
for i in my_list:
result[i] += 1
return result
# this is Frequencies of each nodes.
def find_node(tour):
for i in tour:
if freq[i] != 0:
return i
return -1
def helper(tour, next):
find_path(tour, next)
u = find_node(tour)
while sum(freq) != 0:
sub = find_path([], u)
# get the sub_path
# add them together
# when draw to u, turn to sub, and then come back to go on the original tour path
# [:a], start to a; [a+1:] a+1 to end
tour = tour[:tour.index(u)] + sub + tour[tour.index(u) + 1:]
u = find_node(tour)
return tour
def find_path(tour, next):
for (x, y) in graph:
if x == next:
# from "double-graph"
# pop out the current one and its respondent one
# actually it means we delete this edge
current = graph.pop(graph.index((x,y)))
graph.pop(graph.index((current[1], current[0])))
# now add this "next" node into the tour
tour.append(current[0])
# decrement in frequency
freq[current[0]] -= 1
freq[current[1]] -= 1
return find_path(tour, current[1])
# if this "next" node is not connected to any other nodes
# single one
tour.append(next)
return tour
# in graph, all edges get reversed one and be added to graph
# can call it "double-graph"
# it helps to calculate the frequency in find_path
# actually we can regard frequency as degrees for each node
graph += [(y, x) for (x, y) in graph]
freq = freqencies()
# set graph[0][0] as starting point
return helper([], graph[0][0])
graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)
This solution is optimized for O(V+E) complexity i.e. linear in number of Edges and Vertices in the graph
For those directly wishing to see the code: https://github.com/cubohan/py-algos/blob/master/eulerian_tour.py
Note that the code breaches readability and DRY design majorly but after reading the explanation, you can easily churn out your own version.
# eulerian_tour.py by cubohan
# circa 2017
#
# Problem statement: Given a list of edges, output a list of vertices followed in an eulerian tour
#
# complexity analysis: O(E + V) LINEAR
def find_eulerian_tour(graph):
edges = graph
graph = {}
degree = {}
start = edges[0][0]
count_e = 0
for e in edges:
if not e[0] in graph:
graph[e[0]] = {}
if not e[0] in degree:
degree[e[0]] = 0
if not e[1] in graph:
graph[e[1]] = {}
if not e[1] in degree:
degree[e[1]] = 0
graph[e[0]][e[1]] = 1
graph[e[1]][e[0]] = 1
degree[e[0]] += 1
degree[e[1]] += 1
count_e += 1
max_d = 0
this_ = 0
for v, d in degree.items():
if not d%2 == 0:
# Eulerian tour not possible as odd degree found!
return False
if d>max_d:
this_ = v
max_d = d
visited_e = {}
def is_visited(i, j):
key = str(sorted([i,j]))
if key in visited_e:
return True
else:
visited_e[key] = True
return False
start = this_
route = [start]
indexof = {}
indexof[start] = 0
while count_e>0:
flag = False
for to_v in graph[this_]:
if not is_visited(to_v, this_):
route.append([to_v])
indexof[to_v] = len(route)-1
degree[to_v] -= 1
if degree[to_v] == 0:
del degree[to_v]
degree[this_] -= 1
if degree[this_] == 0:
del degree[this_]
this_ = to_v
flag = True
count_e -= 1
break
if not flag:
break
for key, v in degree.items():
if v <=0:
continue
try:
ind = indexof[key]
except Exception as e:
continue
this_ = key
while count_e>0:
flag = False
for to_v in graph[this_]:
if not is_visited(to_v, this_):
route[ind].append(to_v)
degree[to_v] -= 1
degree[this_] -= 1
this_ = to_v
flag = True
count_e -= 1
break
if not flag:
break
route_ref = []
for r in route:
if type(r) == list:
for _r in r:
route_ref.append(_r)
else:
route_ref.append(r)
return route_ref
if __name__ == "__main__":
print find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5),(4, 8), (1, 6), (3, 7), (5, 9),(2, 4), (0, 4), (2, 5), (3, 6), (8, 9)])
**First the problem can be divided into these subtasks: **
Build the graph into a more friendly structure than a list of edges for easier processing i.e (Adjacency Lists)
Find the degrees of each vertex to first check if an Eulerian Tour is possible (Are there only even degrees? Also, STORE these values in a dict with vertex as key => to be used later)
Build the Eulerian tour
The concept behind my solution is simple.
You choose the vertex with the highest degree as a starting point and set it as the current vertex. (Note: You accomplish this at the same time while calculating degrees of every vertex. You store all these degrees in a dictionary.)
You insert current vertex in the route list which is your answer (Note: also make a dictionary of vertices and their indexes in the route list. This will be used later.)
You visit the first edge of the current vertex if it's not already visited. (Note: A dictionary of visited edges is maintained, the key to this dict is a sorted tuple of the pair of vertices constituting the edge. After visiting an edge, mark it visited by inserting it into the dict.)
You maintain a count of degrees remaining of the current vertex and the visited vertex (This will prove useful later) (Note: you only need to subtract 1 from the dict of degrees you generate before each time you choose an edge)
You switch current vertex to the vertex on the other end of the edge you decided to visit.
Repeat steps 2-5 until you can't find an unvisited edge in the current vertex. (Note: this means you have returned to your starting vertex)
Now consider this: Notice that any unvisited edges/vertices will constitute subgraphs in the main graph which have the same properties as the main graph i.e. an Eulerian tour is possible from any of the vertices in the subgraph starting and ending at the same vertex.
All unvisted edges can be thus visited by taking Eulerian tours in these subgraphs You just need to merge these sub-tours with the first tour.
Next:
You loop through all the vertices of the graph and build a subtour in the same process as listed for the main tour if and only if the reduced degree of this vertex is non-zero
The way these tours will merge with the route list computed previously is that you replace the position of the vertex you're considering to start a subtour from in the route list with the subtour output list and later flattening this route list
We're still not done! What's wrong above?
What happens when you get a non-zero degree vertex which HASN'T BEEN VISITED and isn't present in the route list?!
A caveat: This is an exceptional case.
You might encounter vertices not visited before and thus they will not be present in the main route list.
IGNORE THESE while looping! One of the vertices you have already visited with non-zero reduced degree is GUARANTEED to lead to these vertices in the subtour you will create starting from those.
How is that possible??!!
Draw a graph from the test case given in the link to the code and you will understand. Trace out what your algo is doing every step of the process. Draw it! An image is log(N) complexity to understanding and words O(n2).
Ah, and note that this guarantee holds only if all the edges in the input list form a single graph and not two separate disjointed graphs.
You can mimic the behavior of BFS algorithm and piggyback on it.
Note: I haven't tried write the answer using linked lists because linked lists requires defining 2 classes (one to define nodes and their behaviors, and one to define the entire linked list and its behaviors). However, for more efficiency in (append) and (remove) behaviors, you should use linked lists instead of arrays :
def find_eulerian_tour(graph):
nodes = set()
for i in graph:
if not i[0] in nodes:
nodes.add(i[0])
if not i[1] in nodes:
nodes.add(i[1])
tour = []
tempstack = []
graphtemp = []
current_vertex = graph[0][0]
tour.append(current_vertex)
tempstack.append(current_vertex)
last_edge = ()
count = 0
while len(set(tempstack)) + 1 < len(nodes) or (count == 0 or tour[0] != tour[len(tour) - 1]):
count += 1
flag = False
for edge in graph:
if current_vertex in edge and edge != last_edge:
if current_vertex == edge[0]:
current_vertex = edge[1]
else:
current_vertex = edge[0]
last_edge = edge
graphtemp.append(edge)
graph.remove(edge)
tour.append(current_vertex)
tempstack.append(current_vertex)
flag = True
break
if flag == False:
tour.remove(current_vertex)
current_vertex = tempstack[0]
tempstack.remove(tempstack[0])
graph.append(graphtemp[0])
graphtemp.remove(graphtemp[0])
return tour
print find_eulerian_tour([(1,2), (2,3), (3,1)])
print(find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5), (4, 8), (1, 6), (3, 7), (5, 9), (2, 4), (0, 4), (2, 5), (3, 6), (8, 9)]))
print(find_eulerian_tour([(1, 13), (1, 6), (6, 11), (3, 13), (8, 13), (0, 6), (8, 9),(5, 9), (2, 6), (6, 10), (7, 9), (1, 12), (4, 12), (5, 14), (0, 1), (2, 3), (4, 11), (6, 9), (7, 14), (10, 13)]))
print(find_eulerian_tour([(8, 16), (8, 18), (16, 17), (18, 19), (3, 17), (13, 17), (5, 13),(3, 4), (0, 18), (3, 14), (11, 14), (1, 8), (1, 9), (4, 12), (2, 19),(1, 10), (7, 9), (13, 15), (6, 12), (0, 1), (2, 11), (3, 18), (5, 6), (7, 15), (8, 13), (10, 17)]))
What if we do this? ( just checked and it passed udacity test!!)
import itertools
def create_tour(alist):
return list(itertools.permutations(alist,2))
来源:https://stackoverflow.com/questions/12447880/finding-a-eulerian-tour