""" Looking at graph properties as in chap 3 of Dasgupta. thirty-two:03-02$ python graphs.py -- graph 3p2a -- directed: True cyclic: True linearized: True nodes: node : cc, pre,post = 1, 1,16 node : cc, pre,post = 1, 2,15 node : cc, pre,post = 1, 3,14 node : cc, pre,post = 1, 4,13 node : cc, pre,post = 1, 5,12 node : cc, pre,post = 1, 6,11 node : cc, pre,post = 1, 7,10 node : cc, pre,post = 1, 8,9 edges: edge --: forward edge --: tree edge --: tree edge --: tree edge --: tree edge --: backward edge --: tree edge --: backward edge --: backward edge --: tree edge --: backward edge --: forward edge --: tree -- graph 3p2b -- directed: True cyclic: True linearized: True nodes: node : cc, pre,post = 1, 1,16 node : cc, pre,post = 1, 12,15 node : cc, pre,post = 1, 13,14 node : cc, pre,post = 1, 2,11 node : cc, pre,post = 1, 3,10 node : cc, pre,post = 1, 6,9 node : cc, pre,post = 1, 7,8 node : cc, pre,post = 1, 4,5 edges: edge --: tree edge --: backward edge --: forward edge --: cross edge --: cross edge --: tree edge --: tree edge --: tree edge --: tree edge --: cross edge --: tree edge --: tree edge --: backward Jim M | March 2 2011 | GPL """ from random import randint, sample from pprint import PrettyPrinter from string import uppercase from subprocess import call class Node: def __init__(self, name="", neighbors=None): self.name = name if neighbors: self.neighbors = neighbors else: self.neighbors = [] self.cc_num = -1 # connected component number for undirected graphs self.pre_num = -1 # pre visit number in explore() self.post_num = -1 # post visit number in explore() self.visited = False # depth-first search visited flag def __str__(self): if self.name: return "<" + self.name + ">" else: return "<" + id(self) + ">" class Graph: def __init__(self, nodes = [], directed=False, name=""): self.nodes = nodes self.N = len(nodes) self.name = name self.directed = directed self.cc = 0 # connected component ; see Dasgupta chap 3 self.clock = 1 # pre and post explore clock ; ditto self.searched = False self.init_edges() def edge_type(self, node1, node2): # See dasgupta pg 100 if (node1.pre_num < node1.post_num < node2.pre_num < node2.post_num) or \ (node2.pre_num < node2.post_num < node1.pre_num < node1.post_num): return 'cross' elif node1.post_num < node2.post_num: self.cyclic = True return 'backward' elif self.get_edge_value(node1, node2) == 'tree': # set in explore() return 'tree' else: return 'forward' def set_edge_types(self): for node1 in self.nodes: for node2 in node1.neighbors: self.set_edge_value(node1, node2, self.edge_type(node1, node2)) def edge_name(self, node1, node2): return str(node1) + "--" + str(node2) def set_edge_value(self, node1, node2, value): self.edges[self.edge_name(node1, node2)] = value def get_edge_value(self, node1, node2): return self.edges[self.edge_name(node1, node2)] def init_edges(self): """ Setup self.edges as {node1-node2:None} dict """ self.edges = {} self.cyclic = False # set to true in edge_type if a 'back' edge seen for node1 in self.nodes: for node2 in node1.neighbors: self.set_edge_value(node1, node2, None) def summary(self): """ Return summary description of graph """ if not self.searched: self.depth_first_search() self.linearize_nodes() self.set_edge_types() result = "-- graph %s --\n" % self.name result += " adjacency matrix: \n" result += PrettyPrinter().pformat(self.adjacency_matrix()) + "\n" result += " directed: %s\n" % str(self.directed) result += " cyclic: %s\n" % str(self.cyclic) result += " linearized: %s\n" % str(self.linearized) result += " nodes: \n" for node in self.nodes: result += " node %s: cc, pre,post = %i, %i,%i \n" % \ (str(node), node.cc_num, node.pre_num, node.post_num) result += " edges: \n" for (edge, type) in self.edges.items(): result += " edge %s: %s \n" % (edge, type) return result def linearize_nodes(self): self.nodes.sort(lambda x,y: cmp(y.post_num, x.post_num)) self.linearized = True def depth_first_search(self): """ Explore downward from each unvisited node. """ self.cc = 0 # reset self.clock = 1 # reset for node in self.nodes: node.visited = False # reset for node in self.nodes: if not node.visited: self.cc += 1 self.explore(node) self.searched = True def previsit(self, node): node.cc_num = self.cc node.pre_num = self.clock self.clock += 1 def postvisit(self, node): node.post_num = self.clock self.clock += 1 def explore(self, node): """ Explore depth-first recursively from this node. clock is a counter for pre and post visit numbers; cc is connected component number for undirected graphs.""" node.visited = True self.previsit(node) for neighbor in node.neighbors: if not neighbor.visited: self.set_edge_value(node, neighbor, 'tree') self.explore(neighbor) self.postvisit(node) def adjacency_matrix(self): """ Return N x N matrix giving which node is connected to which """ matrix = [None] * self.N for i in range(self.N): matrix[i] = [None] * self.N for j in range(self.N): if self.nodes[j] in self.nodes[i].neighbors: matrix[i][j] = 1 else: matrix[i][j] = 0 return matrix def graphviz(self, filename=None, fancy=False): """ Return string appropriate for graphviz (dot) plot. If given a filename (i.e. 'graph'), create both graph.dot and from it graph.png. """ if self.directed: result = "digraph {\n" edge = " -> " else: result = "graph {\n" edge = " -- " for node in self.nodes: for neighbor in node.neighbors: if self.directed or id(node) < id(neighbor): if fancy: # graphviz line styles: dashed, dotted, solid, bold; color=red,green,... style = {'forward': "solid", 'backward': "dashed", 'tree': "bold", 'cross': "dotted" }[self.get_edge_value(node, neighbor)] label = '[style=' + style + ']' else: label = "" result += " " + node.name + edge + neighbor.name + label + ";\n" result += "}\n"; if filename: dotfilename = filename + ".dot" pngfilename = filename + ".png" with open(dotfilename, 'w') as dotfile: dotfile.write(result) dotfile.close() command = "dot -Tpng < " + dotfilename + " > " + pngfilename call(command, shell=True) return result class RandomGraph(Graph): def __init__(self, N=6, min_neighbors=2, max_neighbors=4, directed=True): """ Return a random graph with N vertices, each connected to some randomly chosen other vertices. (For a directed graph, the number of neighbors will be randomly chosen from the min to max values. For an undirected one, the neighbor counts will be close to that range when N >> (min, max). """ self.name = "random_%i" % N self.N = N self.directed = directed self.nodes = [] for i in range(N): self.nodes.append(Node(name=str(i))) if not directed: # For undirected graphs, min_neighbors /= 2 # each edge A->B will also adds B->A max_neighbors /= 2 # so start with half as many as requested. for node in self.nodes: n_neighbors = randint(min_neighbors, max_neighbors) node.neighbors = sample(self.nodes, n_neighbors) if node in node.neighbors: # A node is not its own neighbor node.neighbors.remove(node) if not self.directed: for node1 in self.nodes: for node2 in self.nodes: if node1 != node2: if node2 in node1.neighbors: if not node1 in node2.neighbors: node2.neighbors.append(node1) self.init_edges() def graph_from_adjacency(adjacency, itsname): node = {} for _name in adjacency.keys(): node[_name] = Node(name=_name) for (start, ends) in adjacency.items(): for end in ends: node[start].neighbors.append(node[end]) return Graph(nodes=node.values(), directed=True, name=itsname) # dasgupta exercise 3.2a adjacent_3p2a = { 'A': ['B', 'F'], 'B': ['C', 'E'], 'C': ['D'], 'D': ['B', 'H'], 'E': ['D', 'G'], 'F': ['E', 'G'], 'G': ['F'], 'H': ['G'], } # dasgupta exercise 3.2b adjacent_3p2b = { 'A': ['B', 'H'], 'B': ['F'], 'C': ['B'], 'D': ['C', 'E'], 'E': [], 'F': ['C', 'D', 'E'], 'G': ['A', 'B', 'F'], 'H': ['G'], } if __name__ == "__main__": for graph in [graph_from_adjacency(adjacent_3p2a, '3p2a'), graph_from_adjacency(adjacent_3p2a, '3p2b'), RandomGraph(N=20, directed=True)]: graph.depth_first_search() print graph.summary() graph.graphviz(filename=graph.name, fancy=True)