################################################################################ # # This file is part of Gato (Graph Algorithm Toolbox) # # file: PlanarityTest.py # author: Ramazan Buzdemir (buzdemir@zpr.uni-koeln.de) # # Copyright (C) 1998-2005, Alexander Schliep, Winfried Hochstaettler and # Copyright 1998-2001 ZAIK/ZPR, Universitaet zu Koeln # # Contact: schliep@molgen.mpg.de, wh@zpr.uni-koeln.de # # Information: http://gato.sf.net # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You should have received a copy of the GNU Library General Public # License along with this library; if not, write to the Free # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # # # This file is version $Revision: 1.14 $ # from $Date: 2005/02/22 11:12:59 $ # last change by $Author: schliep $. # ################################################################################ ############################################################################### ############################################################################### ############################################################################### # # # AN IMPLEMENTATION OF # # THE HOPCROFT AND TARJAN # # PLANARITY TEST AND EMBEDDING ALGORITHM # # # ############################################################################### # # # References: # # # # [Meh84] K.Mehlhorn. # # "Data Structures and Efficient Algorithms." # # Springer Verlag, 1984. # # [MM94] K.Mehlhorn and P.Mutzel. # # "On the embedding phase of the Hopcroft and Tarjan planarity # # testing algorithm." # # Technical report no. 117/94, mpi, Saarbruecken, 1994 # # # ############################################################################### #=============================================================================# from copy import deepcopy from DataStructures import Stack from tkMessageBox import showinfo #=============================================================================# #=============================================================================# class List: def __init__(self,el=[]): elc=deepcopy(el) self.elements=elc # a) Access Operations def length(self): return len(self.elements) def empty(self): if self.length()==0: return 1 else: return 0 def head(self): return self.elements[0] def tail(self): return self.elements[-1] # b)Update Operations def push(self,x): self.elements.insert(0,x) return x def Push(self,x): self.elements.append(x) return x def append(self,x): self.Push(x) def pop(self): x=self.elements[0] self.elements=self.elements[1:] return x def Pop(self): x=self.elements[-1] self.elements=self.elements[:-1] return x def clear(self): self.elements=[] def conc(self,A): self.elements=self.elements+A.elements A.elements=[] #=============================================================================# #=============================================================================# class pt_graph: def __init__(self): self.V = [] self.E = [] self.adjEdges = {} # a) Access operations def source(self,e): return e[0] def target(self,e): return e[1] def number_of_nodes(self): return len(self.V) def number_of_edges(self): return len(self.E) def all_nodes(self): return self.V def all_edges(self): return self.E def adj_edges(self,v): return self.adjEdges[v] def adj_nodes(self,v): nodelist=[] for e in self.adj_edges(v): nodelist.append(e[1]) return nodelist def first_node(self): return self.V[0] def last_node(self): return self.V[-1] def first_edge(self): return self.E[0] def last_edge(self): return self.E[-1] def first_adj_edge(self,v): if len(self.adj_edges(v))>0: return self.adj_edges(v)[0] else: return None def last_adj_edge(self,v): if len(self.adj_edges(v))>0: return self.adj_edges(v)[-1] else: return None # b) Update operations def new_node(self,v): self.V.append(v) self.adjEdges[v]=[] return v def new_edge(self,v,w): if v==w: # Loop raise GraphNotSimpleError if (v,w) in self.E: # Multiple edge raise GraphNotSimpleError self.E.append((v,w)) self.adjEdges[v].append((v,w)) return (v,w) def del_node(self,v): try: for k in self.V: for e in self.adj_edges(k): if source(e)==v or target(e)==v: self.adjEdges[k].remove(e) self.V.remove(v) for e in self.E: if source(e)==v or target(e)==v: self.E.remove(e) except KeyError: raise NoSuchVertexError def del_edge(self,e): try: self.E.remove(e) self.adjEdges[source(e)].remove((source(e),target(e))) except KeyError: raise NoSuchEdgeError def del_nodes(self,node_list): # deletes all nodes in list L from self L=deepcopy(node_list) for l in L: self.del_node(l) def del_edges(self,edge_list): # deletes all edges in list L from self L=deepcopy(edge_list) for l in L: self.del_edge(l) def del_all_nodes(self): # deletes all nodes from self self.del_nodes(self.all_nodes()) def del_all_edges(self): # deletes all edges from self self.del_edges(self.all_edges()) def sort_edges(self, cost): def up(x,y): if x[1]dfsnum_w0: self.flip() Att.conc(self.Latt) Att.conc(self.Ratt) # This needs some explanation. # Note that |Ratt| is either empty or {w0}. # Also if |Ratt| is non-empty then all subsequent # sets are contained in {w0}. # So we indeed compute an ordered set of attachments. for e in self.Lseg.elements: alpha[e]=left for e in self.Rseg.elements: alpha[e]=right #=============================================================================# #=============================================================================# # GLOBALS: left=1 right=2 G=pt_graph() reached={} dfsnum={} parent={} dfs_count=0 lowpt={} Del=[] lowpt1={} lowpt2={} alpha={} Att=List() cur_nr=0 sort_num={} tree_edge_into={} #=============================================================================# #=============================================================================# def planarity_test(Gin): # planarity_test decides whether the InputGraph is planar. # it also order the adjecentLists in counterclockwise. ### SaveGmlGraph(Gin,"/home/ramazan/Leda/Graphs/rama.gml") n=Gin.Order() # number of nodes if n<3: return 1 if not(Gin.QDirected()) and Gin.Size()>3*n-6: return 0 # number of edges if Gin.QDirected() and Gin.Size()>6*n-12: return 0 #-------------------------------------------------------------- # make G a copy of Gin and make G bidirected global G,cur_nr G=pt_graph() for v in Gin.vertices: G.new_node(v) for e in Gin.Edges(): G.new_edge(source(e),target(e)) cur_nr=0 nr={} cost={} n=G.number_of_nodes() for v in G.all_nodes(): nr[v]=cur_nr cur_nr=cur_nr+1 for e in G.all_edges(): if nr[source(e)] < nr[target(e)]: cost[e]=n*nr[source(e)] + nr[target(e)] else: cost[e]=n*nr[target(e)] + nr[source(e)] G.sort_edges(cost) L=List(G.all_edges()) while not(L.empty()): e=L.pop() if (not(L.empty()) and source(e)==target(L.head()) and source(L.head())==target(e)): L.pop() else: G.new_edge(target(e),source(e)) #-------------------------------------------------------------- #-------------------------------------------------------------- # make G biconnected Make_biconnected_graph() #-------------------------------------------------------------- #-------------------------------------------------------------- # make H a copy of G # # We need the biconnected version of G (G will be further modified # during the planarity test) in order to construct the planar embedding. # So we store it as a graph H. H=deepcopy(G) #-------------------------------------------------------------- #-------------------------------------------------------------- # test planarity global dfsnum,parent,alpha,Att dfsnum={} parent={} for v in G.all_nodes(): parent[v]=None reorder() alpha={} for e in G.all_edges(): alpha[e]=0 Att=List() alpha[G.first_adj_edge(G.first_node())] = left if not(strongly_planar(G.first_adj_edge(G.first_node()),Att)): return 0 #-------------------------------------------------------------- #-------------------------------------------------------------- # construct embedding global sort_num,tree_edge_into T=List() A=List() cur_nr=0 sort_num={} tree_edge_into={} embedding(G.first_adj_edge(G.first_node()),left,T,A) # |T| contains all edges incident to the first node except the # cycle edge into it. # That edge comprises |A|. T.conc(A) for e in T.elements: sort_num[e]=cur_nr cur_nr=cur_nr+1 H.sort_edges(sort_num) #-------------------------------------------------------------- return H.all_edges() # ccwOrderedEges #=============================================================================# #=============================================================================# def pt_DFS(v): global G,reached S=Stack() if reached[v]==0: reached[v]=1 S.Push(v) while S.IsNotEmpty(): v=S.Pop() for w in G.adj_nodes(v): if reached[w]==0: reached[w]=1 S.Push(w) #=============================================================================# #=============================================================================# def Make_biconnected_graph(): # We first make it connected by linking all roots of a DFS-forest. # Assume now that G is connected. # Let a be any articulation point and let u and v be neighbors # of a belonging to different biconnected components. # Then there are embeddings of the two components with the edges # {u,a} and {v,a} on the boundary of the unbounded face. # Hence we may add the edge {u,v} without destroying planarity. # Proceeding in this way we make G biconnected. global G,reached,dfsnum,parent,dfs_count,lowpt #-------------------------------------------------------------- # We first make G connected by linking all roots of the DFS-forest. reached={} for v in G.all_nodes(): reached[v]=0 u=G.first_node() for v in G.all_nodes(): if not(reached[v]): # explore the connected component with root v pt_DFS(v) if u!=v: # link v's component to the first component G.new_edge(u,v) G.new_edge(v,u) #-------------------------------------------------------------- #-------------------------------------------------------------- # We next make G biconnected. for v in G.all_nodes(): reached[v]=0 dfsnum={} parent={} for v in G.all_nodes(): parent[v]=None dfs_count=0 lowpt={} dfs_in_make_biconnected_graph(G.first_node()) #-------------------------------------------------------------- #=============================================================================# #=============================================================================# def dfs_in_make_biconnected_graph(v): # This procedure determines articulation points and adds appropriate # edges whenever it discovers one. global G,reached,dfsnum,parent,dfs_count,lowpt dfsnum[v]=dfs_count dfs_count=dfs_count+1 lowpt[v]=dfsnum[v] reached[v]=1 if not(G.first_adj_edge(v)): return # no children u=target(G.first_adj_edge(v)) # first child for e in G.adj_edges(v): w=target(e) if not(reached[w]): # e is a tree edge parent[w]=v dfs_in_make_biconnected_graph(w) if lowpt[w]==dfsnum[v]: # v is an articulation point. We now add an edge. # If w is the first child and v has a parent then we # connect w and parent[v], if w is a first child and v # has no parent then we do nothing. # If w is not the first child then we connect w to the # first child. # The net effect of all of this is to link all children # of an articulation point to the first child and the # first child to the parent (if it exists). if w==u and parent[v]: G.new_edge(w,parent[v]) G.new_edge(parent[v],w) if w!=u: G.new_edge(u,w) G.new_edge(w,u) lowpt[v]=min(lowpt[v],lowpt[w]) else: lowpt[v]=min(lowpt[v],dfsnum[w]) # non tree edge #=============================================================================# #=============================================================================# def reorder(): # The procedure reorder first performs DFS to compute dfsnum, parent # lowpt1 and lowpt2, and the list Del of all forward edges and all # reversals of tree edges. # It then deletes the edges in Del and finally reorders the edges. global G,dfsnum,parent,reached,dfs_count,Del,lowpt1,lowpt2 reached={} for v in G.all_nodes(): reached[v]=0 dfs_count = 0 Del=[] lowpt1={} lowpt2={} dfs_in_reorder(G.first_node()) #-------------------------------------------------------------- # remove forward and reversals of tree edges for e in Del: G.del_edge(e) #-------------------------------------------------------------- #-------------------------------------------------------------- # we now reorder adjacency lists cost={} for e in G.all_edges(): v = source(e) w = target(e) if dfsnum[w]=dfsnum[v]: cost[e]=2*lowpt1[w] else: cost[e]=2*lowpt1[w]+1 G.sort_edges(cost) #-------------------------------------------------------------- #=============================================================================# #=============================================================================# def dfs_in_reorder(v): global G,dfsnum,parent,reached,dfs_count,Del,lowpt1,lowpt2 #-------------------------------------------------------------- dfsnum[v]=dfs_count dfs_count=dfs_count+1 lowpt1[v]=lowpt2[v]=dfsnum[v] reached[v]=1 for e in G.adj_edges(v): w = target(e); if not(reached[w]): # e is a tree edge parent[w]=v dfs_in_reorder(w) lowpt1[v]=min(lowpt1[v],lowpt1[w]) else: lowpt1[v]=min(lowpt1[v],dfsnum[w]) # no effect for forward edges if dfsnum[w]>=dfsnum[v] or w==parent[v]: # forward edge or reversal of tree edge Del.append(e) #-------------------------------------------------------------- #-------------------------------------------------------------- # we know |lowpt1[v]| at this point and now make a second pass over all # adjacent edges of |v| to compute |lowpt2| for e in G.adj_edges(v): w = target(e) if parent[w]==v: # tree edge if lowpt1[w]!=lowpt1[v]: lowpt2[v]=min(lowpt2[v],lowpt1[w]) lowpt2[v]=min(lowpt2[v],lowpt2[w]) else: # all other edges if lowpt1[v]!=dfsnum[w]: lowpt2[v]=min(lowpt2[v],dfsnum[w]) #-------------------------------------------------------------- #=============================================================================# #=============================================================================# def strongly_planar(e0,Att): # We now come to the heart of the planarity test: procedure strongly_planar. # It takes a tree edge e0=(x,y) and tests whether the segment S(e0) is # strongly planar. # If successful it returns (in Att) the ordered list of attachments of S(e0) # (excluding x); high DFS-numbers are at the front of the list. # In alpha it records the placement of the subsegments. # # strongly_planar operates in three phases. # It first constructs the cycle C(e0) underlying the segment S(e0). # It then constructs the interlacing graph for the segments emanating >from the # spine of the cycle. # If this graph is non-bipartite then the segment S(e0) is non-planar. # If it is bipartite then the segment is planar. # In this case the third phase checks whether the segment is strongly planar # and, if so, computes its list of attachments. global G,alpha,dfsnum,parent #-------------------------------------------------------------- # DETERMINE THE CYCLE C(e0) # We determine the cycle "C(e0)" by following first edges until a back # edge is encountered. # |wk| will be the last node on the tree path and |w0| # is the destination of the back edge. x=source(e0) y=target(e0) e=G.first_adj_edge(y) wk=y while dfsnum[target(e)]>dfsnum[wk]: # e is a tree edge wk=target(e) e=G.first_adj_edge(wk) w0=target(e) #-------------------------------------------------------------- #-------------------------------------------------------------- # PROCESS ALL EDGES LEAVING THE SPINE # The second phase of |strongly_planar| constructs the connected # components of the interlacing graph of the segments emananating # from the the spine of the cycle "C(e0)". # We call a connected component a "block". # For each block we store the segments comprising its left and # right side (lists |Lseg| and |Rseg| contain the edges defining # these segments) and the ordered list of attachments of the segments # in the block; # lists |Latt| and |Ratt| contain the DFS-numbers of the attachments; # high DFS-numbers are at the front of the list. # # We process the edges leaving the spine of "S(e0)" starting at # node |wk| and working backwards. # The interlacing graph of the segments emanating from # the cycle is represented as a stack |S| of blocks. w=wk S=Stack() while w!=x: count=0 for e in G.adj_edges(w): count=count+1 if count!=1: # no action for first edge # TEST RECURSIVELY # Let "e" be any edge leaving the spine. # We need to test whether "S(e)" is strongly planar # and if so compute its list |A| of attachments. # If "e" is a tree edge we call our procedure recursively # and if "e" is a back edge then "S(e)" is certainly strongly # planar and |target(e)| is the only attachment. # If we detect non-planarity we return false and free # the storage allocated for the blocks of stack |S|. A=List() if dfsnum[w]dfsnum[w0] and B.head_of_Ratt()>dfsnum[w0]): del B while S.IsNotEmpty(): S.Pop() return 0 B.add_to_Att(Att,dfsnum[w0],alpha,dfsnum) del B # Let's not forget that "w0" is an attachment # of "S(e0)" except if w0 = x. if w0!=x: Att.append(dfsnum[w0]) return 1 #-------------------------------------------------------------- #=============================================================================# #=============================================================================# def embedding(e0,t,T,A): # embed: determine the cycle "C(e0)" # # We start by determining the spine cycle. # This is precisley as in |strongly_planar|. # We also record for the vertices w_r+1, ...,w_k, and w_0 the # incoming cycle edge either in |tree_edge_into| or in the local # variable |back_edge_into_w0|. global G,dfsnum,cur_nr,sort_num,tree_edge_into,parent x=source(e0) y=target(e0) tree_edge_into[y]=e0 e=G.first_adj_edge(y) wk=y while (dfsnum[target(e)]>dfsnum[wk]): # e is a tree edge wk=target(e) tree_edge_into[wk]=e e=G.first_adj_edge(wk) w0=target(e) back_edge_into_w0=e # process the subsegments w=wk Al=List() Ar=List() Tprime=List() Aprime=List() T.clear() T.append(e) # |e=(wk,w0)| at this point while w!=x: count=0 for e in G.adj_edges(w): count=count+1 if count!=1: # no action for first edge # embed recursively if dfsnum[w][%i]" %((source(e)-1),(target(e)-1)) print for v in G.all_nodes(): print "[%i] : " %(v-1) for e in G.adj_edges(v): print " [%i]---->[%i]" %((source(e)-1),(target(e)-1)) print #=============================================================================# #=============================================================================# def SaveGmlGraph(G, fileName): file = open(fileName, 'w') file.write("graph [ \n") file.write(" directed 1 \n \n") for v in G.vertices: file.write(" node [ id ") n=str(v) file.write(n) file.write(" ] \n") file.write("\n") for e in G.Edges(): file.write(" edge [ \n") file.write(" source ") k=str(e[0]) file.write(k) file.write("\n") file.write(" target ") k=str(e[1]) file.write(k) file.write("\n") file.write(" ] \n") file.write("] \n") #=============================================================================#