def is_product(n, den_tuple): r""" INPUT: - ``n`` - number of variables - ``den_tuple`` - tuple of pairs ``(vector, power)`` TESTS:: sage: from surface_dynamics.misc.generalized_multiple_zeta_values import is_product sage: is_product(3, [((1,0,0),2), ((0,1,0),5), ((1,1,0),1), ((0,0,1),3)]) [(2, (((0, 1), 5), ((1, 0), 2), ((1, 1), 1))), (1, (((1), 3),))] sage: is_product(3, [((1,0,0),2), ((0,1,0),3), ((1,0,1),1), ((0,0,1),5)]) [(2, (((0, 1), 5), ((1, 0), 2), ((1, 1), 1))), (1, (((1), 3),))] sage: is_product(3, [((1,1,1),3)]) is None True """ D = DisjointSet(n) assert all(len(v) == n for v, p in den_tuple), (n, den_tuple) # 1. product structure for v, _ in den_tuple: i0 = 0 while not v[i0]: i0 += 1 i = i0 + 1 while i < n: if v[i]: D.union(i0, i) i += 1 if D.number_of_subsets() == 1: # no way to split variables return # split variables Rdict = D.root_to_elements_dict() keys = sorted(Rdict.keys()) key_indices = {k: i for i, k in enumerate(keys)} values = [Rdict[k] for k in keys] values_indices = [{v: i for i, v in enumerate(v)} for v in values] n_list = [len(J) for J in values] F = [FreeModule(ZZ, nn) for nn in n_list] new_terms = [[] for _ in range(len(Rdict))] for v, p in den_tuple: i0 = 0 while not v[i0]: i0 += 1 i0 = D.find(i0) assert all(D.find(i) == i0 for i in range(n) if v[i]), (i0, [D.find(i) for i in range(n) if v[i]]) k = key_indices[i0] vv = F[k]() for i in range(n): if v[i]: vv[values_indices[k][i]] = v[i] vv.set_immutable() new_terms[k].append((vv, p)) return list(zip(n_list, [tuple(sorted(terms)) for terms in new_terms]))
def decomposition(self): """ Find the decomposition of the matroid as a direct sum of indecomposable matroids. Return a partition of the groundset. Uses the algorithm of [PP19, Section 7]. """ B = self.basis() new_groundset = list(B) + list(self.groundset().difference(B)) # construct matrix with permuted columns columns = [vector(self._A[:,self._groundset_to_index[e]]) for e in new_groundset] A = matrix(ZZ, columns).transpose().echelon_form() uf = DisjointSet(self.groundset()) for i in xrange(A.nrows()): for j in xrange(i+1, A.ncols()): if A[i,j] != 0: uf.union(new_groundset[i], new_groundset[j]) return SetPartition(uf)
def is_partial_cube(G, certificate=False): r""" Test whether the given graph is a partial cube. A partial cube is a graph that can be isometrically embedded into a hypercube, i.e., its vertices can be labelled with (0,1)-vectors of some fixed length such that the distance between any two vertices in the graph equals the Hamming distance of their labels. Originally written by D. Eppstein for the PADS library (http://www.ics.uci.edu/~eppstein/PADS/), see also [Epp2008]_. The algorithm runs in `O(n^2)` time, where `n` is the number of vertices. See the documentation of :mod:`~sage.graphs.partial_cube` for an overview of the algorithm. INPUT: - ``certificate`` -- boolean (default: ``False``); this function returns ``True`` or ``False`` according to the graph, when ``certificate = False``. When ``certificate = True`` and the graph is a partial cube, the function returns ``(True, mapping)``, where ``mapping`` is an isometric mapping of the vertices of the graph to the vertices of a hypercube ((0, 1)-strings of a fixed length). When ``certificate = True`` and the graph is not a partial cube, ``(False, None)`` is returned. EXAMPLES: The Petersen graph is not a partial cube:: sage: g = graphs.PetersenGraph() sage: g.is_partial_cube() False All prisms are partial cubes:: sage: g = graphs.CycleGraph(10).cartesian_product(graphs.CompleteGraph(2)) sage: g.is_partial_cube() True TESTS: The returned mapping is an isometric embedding into a hypercube:: sage: g = graphs.DesarguesGraph() sage: _, m = g.is_partial_cube(certificate=True) sage: m # random {0: '00000', 1: '00001', 2: '00011', 3: '01011', 4: '11011', 5: '11111', 6: '11110', 7: '11100', 8: '10100', 9: '00100', 10: '01000', 11: '10001', 12: '00111', 13: '01010', 14: '11001', 15: '10111', 16: '01110', 17: '11000', 18: '10101', 19: '00110'} sage: all(all(g.distance(u, v) == len([i for i in range(len(m[u])) if m[u][i] != m[v][i]]) for v in m) for u in m) True A graph without vertices is trivially a partial cube:: sage: Graph().is_partial_cube(certificate=True) (True, {}) """ G._scream_if_not_simple() if not G.order(): if certificate: return (True, {}) else: return True if certificate: fail = (False, None) else: fail = False if not G.is_connected(): return fail n = G.order() # Initial sanity check: are there few enough edges? # Needed so that we don't try to use union-find on a dense # graph and incur superquadratic runtimes. if 1 << (2 * G.size() // n) > n: return fail # Check for bipartiteness. # This ensures also that each contraction will be bipartite. if not G.is_bipartite(): return fail # Set up data structures for algorithm: # - contracted: contracted graph at current stage of algorithm # - unionfind: union find data structure representing known edge equivalences # - available: limit on number of remaining available labels from sage.graphs.digraph import DiGraph from sage.graphs.graph import Graph from sage.sets.disjoint_set import DisjointSet contracted = DiGraph({v: {w: (v, w) for w in G[v]} for v in G}) unionfind = DisjointSet(contracted.edges(labels=False)) available = n - 1 # Main contraction loop in place of the original algorithm's recursion while contracted.order() > 1: # Find max degree vertex in contracted, and update label limit deg, root = max([(contracted.out_degree(v), v) for v in contracted], key=lambda x: x[0]) if deg > available: return fail available -= deg # Set up bitvectors on vertices bitvec = {v: 0 for v in contracted} neighbors = {} for i, neighbor in enumerate(contracted[root]): bitvec[neighbor] = 1 << i neighbors[1 << i] = neighbor # Breadth first search to propagate bitvectors to the rest of the graph for level in breadth_first_level_search(contracted, root): for v in level: for w in level[v]: bitvec[w] |= bitvec[v] # Make graph of labeled edges and union them together labeled = Graph([contracted.vertices(), []]) for v, w in contracted.edge_iterator(labels=False): diff = bitvec[v] ^ bitvec[w] if not diff or not bitvec[w] & ~bitvec[v]: continue # zero edge or wrong direction if diff not in neighbors: return fail neighbor = neighbors[diff] unionfind.union(contracted.edge_label(v, w), contracted.edge_label(root, neighbor)) unionfind.union(contracted.edge_label(w, v), contracted.edge_label(neighbor, root)) labeled.add_edge(v, w) # Map vertices to components of labeled-edge graph component = {} for i, SCC in enumerate(labeled.connected_components()): for v in SCC: component[v] = i # generate new compressed subgraph newgraph = DiGraph() for v, w, t in contracted.edge_iterator(): if bitvec[v] == bitvec[w]: vi = component[v] wi = component[w] if vi == wi: return fail if newgraph.has_edge(vi, wi): unionfind.union(newgraph.edge_label(vi, wi), t) else: newgraph.add_edge(vi, wi, t) contracted = newgraph # Make a digraph with edges labeled by the equivalence classes in unionfind g = DiGraph({v: {w: unionfind.find((v, w)) for w in G[v]} for v in G}) # Associates to a vertex the token that acts on it, an check that # no two edges on a single vertex have the same label action = {} for v in g: action[v] = set(t for _, _, t in g.edge_iterator(v)) if len(action[v]) != g.out_degree(v): return fail # Associate every token to its reverse reverse = {} for v, w, t in g.edge_iterator(): rt = g.edge_label(w, v) reverse[t] = rt reverse[rt] = t current = initialState = next(g.vertex_iterator()) # A token T is said to be 'active' for a vertex u if it takes u # one step closer to the source in terms of distance. The 'source' # is initially 'initialState'. See the module's documentation for # more explanations. # Find list of tokens that lead to the initial state activeTokens = set() for level in breadth_first_level_search(g, initialState): for v in level: for w in level[v]: activeTokens.add(g.edge_label(w, v)) for t in activeTokens: if reverse[t] in activeTokens: return fail activeTokens = list(activeTokens) # Rest of data structure: point from states to list and list to states state_to_active_token = {v: -1 for v in g} token_to_states = [[] for i in activeTokens ] # (i.e. vertices on which each token acts) def scan(v): """Find the next token that is effective for v.""" a = next( i for i in range(state_to_active_token[v] + 1, len(activeTokens)) if activeTokens[i] is not None and activeTokens[i] in action[v]) state_to_active_token[v] = a token_to_states[a].append(v) # Initialize isometric embedding into a hypercube if certificate: dim = 0 tokmap = {} for t in reverse: if t not in tokmap: tokmap[t] = tokmap[reverse[t]] = 1 << dim dim += 1 embed = {initialState: 0} # Set initial active states for v in g: if v != current: try: scan(v) except StopIteration: return fail # Traverse the graph, maintaining active tokens for prev, current, fwd in depth_first_traversal(g, initialState): if not fwd: prev, current = current, prev elif certificate: embed[current] = embed[prev] ^ tokmap[g.edge_label(prev, current)] # Add token to end of list, point to it from old state activeTokens.append(g.edge_label(prev, current)) state_to_active_token[prev] = len(activeTokens) - 1 token_to_states.append([prev]) # Inactivate reverse token, find new token for its states # # (the 'active' token of 'current' is necessarily the label of # (current, previous)) activeTokens[state_to_active_token[current]] = None for v in token_to_states[state_to_active_token[current]]: if v != current: try: scan(v) except StopIteration: return fail # All checks passed, return the result if certificate: format = "{0:0%db}" % dim return (True, {v: format.format(l) for v, l in embed.items()}) else: return True
def poset_of_layers(self): """ Compute the poset of layers of the associated toric arrangement, using Lenz's algorithm [Len17a]. """ # TODO: implement for Q != 0 if self._Q.ncols() > 0: raise NotImplementedError A = self._A.transpose() E = range(A.nrows()) data = {} # compute Smith normal forms of all submatrices for S in powerset(E): D, U, V = A[S,:].smith_form() # D == U*A[S,:]*V diagonal = [D[i,i] if i < D.ncols() else 0 for i in xrange(len(S))] data[tuple(S)] = (diagonal, U) # generate al possible elements of the poset of layers elements = {tuple(S): list(vector(ZZ, x) for x in itertools.product(*(range(max(data[tuple(S)][0][i], 1)) for i in xrange(len(S))))) for S in powerset(E)} for l in elements.itervalues(): for v in l: v.set_immutable() possible_layers = list((S, x) for (S, l) in elements.iteritems() for x in l) uf = DisjointSet(possible_layers) cover_relations = [] for (S, l) in elements.iteritems(): diagonal_S, U_S = data[S] rk_S = A[S,:].rank() for s in S: i = S.index(s) # index where the element s appears in S T = tuple(t for t in S if t != s) diagonal_T, U_T = data[T] rk_T = A[T,:].rank() for x in l: h = (S, x) y = U_S**(-1) * x z = U_T * vector(ZZ, y[:i].list() + y[i+1:].list()) w = vector(ZZ, (a % diagonal_T[j] if diagonal_T[j] > 0 else 0 for j, a in enumerate(z))) w.set_immutable() ph = (T, w) if rk_S == rk_T: uf.union(h, ph) else: cover_relations.append((ph, h)) # find representatives for layers (we keep the representative (S,x) with maximal S) root_to_representative_dict = {} for root, subset in uf.root_to_elements_dict().iteritems(): S, x = max(subset, key=lambda (S, x): len(S)) S_labeled = tuple(self._E[i] for i in S) root_to_representative_dict[root] = (S_labeled, x) # get layers and cover relations layers = root_to_representative_dict.values() cover_relations = set( (root_to_representative_dict[uf.find(a)], root_to_representative_dict[uf.find(b)]) for (a,b) in cover_relations) return Poset(data=(layers, cover_relations), cover_relations=True)
def is_equivalent(self, other, morphism=None): """ Check if the two ToricArithmeticMatroids are equivalent, i.e. the defining representations are equivalent (see [PP19, Section 2]). If morphism is None, assume that the groundsets coincide. """ if not isinstance(other, ToricArithmeticMatroid): raise TypeError("can only test for equivalence between toric arithmetic matroids.") if self._Q.ncols() > 0 or other._Q.ncols() > 0: # TODO raise NotImplementedError if morphism is None: assert self.groundset() == other.groundset() morphism = {e: e for e in self.groundset()} E = self._E # take matrices in Hermite normal form, removing zero rows # (copy is needed to make matrices mutable) M = copy.copy(self._A.echelon_form(include_zero_rows=False)) N = copy.copy(other._A[:, [other._groundset_to_index[morphism[e]] for e in self._E]].echelon_form(include_zero_rows=False)) # choose a basis B = self.basis() if not other.is_basis(frozenset(morphism[e] for e in B)): return False # find bipartite graph edges = [] for x in B: for y in E: C = B.difference([x]).union([y]) if y not in B and self.is_basis(C): if not other.is_basis(frozenset(morphism[e] for e in C)): return False edges.append((x,y)) spanning_forest = nx.Graph() # find spanning forest uf = DisjointSet(E) for (x,y) in edges: if uf.find(x) != uf.find(y): spanning_forest.add_edge(x,y) uf.union(x,y) B_indices = list(sorted(self._groundset_to_index[e] for e in B)) M1 = M[:, B_indices].inverse() * M N1 = N[:, B_indices].inverse() * N def change_signs(A, A1): for (i,j) in nx.edge_dfs(spanning_forest): (x,y) = (i,j) if i in B else (j,i) if A1[x,y] < 0: if j in B: # change sign of row j and column j A1[j,:] *= -1 A1[:,j] *= -1 A[j,:] *= -1 A[:,j] *= -1 assert A1 == A[:, B_indices].inverse() * A else: # change sign of column j A1[:,j] *= -1 A[:,j] *= -1 change_signs(M, M1) change_signs(N, N1) return M.echelon_form() == N.echelon_form()
def _representation_surjective(self, ordered_groundset=None, check_bases=False): """ Find a representation (if it exists) for a surjective matroid (m(emptyset)=m(E)=1). If check_bases==True, find a representation of a matroid (E,rk,m') such that m'(B)=m(B) for every basis B. Return None if no representation exists. """ assert self.full_multiplicity() == 1 r = self.full_rank() n = len(self.groundset()) if ordered_groundset is not None: # use the groundset in the given order E = ordered_groundset assert frozenset(E) == self.groundset() assert len(E) == len(self.groundset()) else: # sort the groundset E = list(sorted(self.groundset())) B = self.basis() # print "Basis:", B # find bipartite graph edges = [(x,y) for x in B for y in E if y not in B and self.is_basis(B.difference([x]).union([y]))] spanning_forest = nx.Graph() # find spanning forest uf = DisjointSet(E) for (x,y) in edges: if uf.find(x) != uf.find(y): spanning_forest.add_edge(x,y) uf.union(x,y) # print "Graph:", edges # print "Spanning forest:", spanning_forest.edges() # fix an order of B B_ordered = list(sorted(B)) # compute entries of matrix A def entry(i,j): x = B_ordered[i] y = E[j] if y in B: return self.multiplicity(B) if x == y else 0 elif (x,y) in edges: return self.multiplicity(B.difference([x]).union([y])) else: return 0 A = matrix(ZZ, r, n, entry) # print A B_to_index = {B_ordered[i]: i for i in xrange(r)} E_to_index = {E[j]: j for j in xrange(n)} graph = spanning_forest while graph.number_of_edges() < len(edges): # find all paths in the graph paths = dict(nx.all_pairs_dijkstra_path(graph)) for (x,y) in sorted(edges, key=lambda (x,y): len(paths[x][y])): if len(paths[x][y]) == 2: # (x,y) is in the graph assert (x,y) in graph.edges() continue i = B_to_index[x] j = E_to_index[y] rows = [B_to_index[z] for z in paths[x][y][::2]] columns = [E_to_index[z] for z in paths[x][y][1::2]] # print x, y # print "rows:", rows # print "columns:", columns new_tuple = [z for z in B_ordered + paths[x][y] if z not in B or z not in paths[x][y]] # print "new_tuple:", new_tuple expected_mult = self.multiplicity(new_tuple) * self.multiplicity(B)**(len(rows)-1) if self.rank(new_tuple) == r else 0 if abs(A[rows,columns].determinant()) != expected_mult: # change sign # print "change sign!" # print A[rows,columns].determinant() A[i,j] = -A[i,j] if abs(A[rows,columns].determinant()) != expected_mult: # print A # print A[rows,columns].determinant(), expected_mult return None graph.add_edge(x,y) break D, U, V = A.smith_form() res = V.inverse()[:r,:] res = matrix(ZZ, res) # print >> sys.stderr, "Candidate representation:" # print >> sys.stderr, res # check if this is indeed a representation if not self.check_representation(res, ordered_groundset=ordered_groundset, check_bases=check_bases): return None return res
def is_partial_cube(G, certificate=False): r""" Test whether the given graph is a partial cube. A partial cube is a graph that can be isometrically embedded into a hypercube, i.e., its vertices can be labelled with (0,1)-vectors of some fixed length such that the distance between any two vertices in the graph equals the Hamming distance of their labels. Originally written by D. Eppstein for the PADS library (http://www.ics.uci.edu/~eppstein/PADS/), see also [Eppstein2008]_. The algorithm runs in `O(n^2)` time, where `n` is the number of vertices. See the documentation of :mod:`~sage.graphs.partial_cube` for an overview of the algorithm. INPUT: - ``certificate`` (boolean; ``False``) -- The function returns ``True`` or ``False`` according to the graph, when ``certificate = False``. When ``certificate = True`` and the graph is a partial cube, the function returns ``(True, mapping)``, where ``mapping`` is an isometric mapping of the vertices of the graph to the vertices of a hypercube ((0, 1)-strings of a fixed length). When ``certificate = True`` and the graph is not a partial cube, ``(False, None)`` is returned. EXAMPLES: The Petersen graph is not a partial cube:: sage: g = graphs.PetersenGraph() sage: g.is_partial_cube() False All prisms are partial cubes:: sage: g = graphs.CycleGraph(10).cartesian_product(graphs.CompleteGraph(2)) sage: g.is_partial_cube() True TESTS: The returned mapping is an isometric embedding into a hypercube:: sage: g = graphs.DesarguesGraph() sage: _, m = g.is_partial_cube(certificate = True) sage: m # random {0: '00000', 1: '00001', 2: '00011', 3: '01011', 4: '11011', 5: '11111', 6: '11110', 7: '11100', 8: '10100', 9: '00100', 10: '01000', 11: '10001', 12: '00111', 13: '01010', 14: '11001', 15: '10111', 16: '01110', 17: '11000', 18: '10101', 19: '00110'} sage: all(all(g.distance(u, v) == len([i for i in range(len(m[u])) if m[u][i] != m[v][i]]) for v in m) for u in m) True A graph without vertices is trivially a partial cube:: sage: Graph().is_partial_cube(certificate = True) (True, {}) """ G._scream_if_not_simple() if G.order() == 0: if certificate: return (True, {}) else: return True if certificate: fail = (False, None) else: fail = False if not G.is_connected(): return fail n = G.order() # Initial sanity check: are there few enough edges? # Needed so that we don't try to use union-find on a dense # graph and incur superquadratic runtimes. if 1 << (2*G.size()//n) > n: return fail # Check for bipartiteness. # This ensures also that each contraction will be bipartite. if not G.is_bipartite(): return fail # Set up data structures for algorithm: # - contracted: contracted graph at current stage of algorithm # - unionfind: union find data structure representing known edge equivalences # - available: limit on number of remaining available labels from sage.graphs.digraph import DiGraph from sage.graphs.graph import Graph from sage.sets.disjoint_set import DisjointSet contracted = DiGraph({v: {w: (v, w) for w in G[v]} for v in G}) unionfind = DisjointSet(contracted.edges(labels = False)) available = n-1 # Main contraction loop in place of the original algorithm's recursion while contracted.order() > 1: # Find max degree vertex in contracted, and update label limit deg, root = max((contracted.out_degree(v), v) for v in contracted) if deg > available: return fail available -= deg # Set up bitvectors on vertices bitvec = {v:0 for v in contracted} neighbors = {} for i, neighbor in enumerate(contracted[root]): bitvec[neighbor] = 1 << i neighbors[1 << i] = neighbor # Breadth first search to propagate bitvectors to the rest of the graph for level in breadth_first_level_search(contracted, root): for v in level: for w in level[v]: bitvec[w] |= bitvec[v] # Make graph of labeled edges and union them together labeled = Graph([contracted.vertices(), []]) for v, w in contracted.edge_iterator(labels = False): diff = bitvec[v]^bitvec[w] if not diff or bitvec[w] &~ bitvec[v] == 0: continue # zero edge or wrong direction if diff not in neighbors: return fail neighbor = neighbors[diff] unionfind.union(contracted.edge_label(v, w), contracted.edge_label(root, neighbor)) unionfind.union(contracted.edge_label(w, v), contracted.edge_label(neighbor, root)) labeled.add_edge(v, w) # Map vertices to components of labeled-edge graph component = {} for i, SCC in enumerate(labeled.connected_components()): for v in SCC: component[v] = i # generate new compressed subgraph newgraph = DiGraph() for v, w, t in contracted.edge_iterator(): if bitvec[v] == bitvec[w]: vi = component[v] wi = component[w] if vi == wi: return fail if newgraph.has_edge(vi, wi): unionfind.union(newgraph.edge_label(vi, wi), t) else: newgraph.add_edge(vi, wi, t) contracted = newgraph # Make a digraph with edges labeled by the equivalence classes in unionfind g = DiGraph({v: {w: unionfind.find((v, w)) for w in G[v]} for v in G}) # Associates to a vertex the token that acts on it, an check that # no two edges on a single vertex have the same label action = {} for v in g: action[v] = set(t for _, _, t in g.edge_iterator(v)) if len(action[v]) != g.out_degree(v): return fail # Associate every token to its reverse reverse = {} for v, w, t in g.edge_iterator(): rt = g.edge_label(w, v) reverse[t] = rt reverse[rt] = t current = initialState = next(g.vertex_iterator()) # A token T is said to be 'active' for a vertex u if it takes u # one step closer to the source in terms of distance. The 'source' # is initially 'initialState'. See the module's documentation for # more explanations. # Find list of tokens that lead to the initial state activeTokens = set() for level in breadth_first_level_search(g, initialState): for v in level: for w in level[v]: activeTokens.add(g.edge_label(w, v)) for t in activeTokens: if reverse[t] in activeTokens: return fail activeTokens = list(activeTokens) # Rest of data structure: point from states to list and list to states state_to_active_token = {v: -1 for v in g} token_to_states = [[] for i in activeTokens] # (i.e. vertices on which each token acts) def scan(v): """Find the next token that is effective for v.""" a = next(i for i in range(state_to_active_token[v]+1, len(activeTokens)) if activeTokens[i] is not None and activeTokens[i] in action[v]) state_to_active_token[v] = a token_to_states[a].append(v) # Initialize isometric embedding into a hypercube if certificate: dim = 0 tokmap = {} for t in reverse: if t not in tokmap: tokmap[t] = tokmap[reverse[t]] = 1 << dim dim += 1 embed = {initialState: 0} # Set initial active states for v in g: if v != current: try: scan(v) except StopIteration: return fail # Traverse the graph, maintaining active tokens for prev, current, fwd in depth_first_traversal(g, initialState): if not fwd: prev, current = current, prev elif certificate: embed[current] = embed[prev] ^ tokmap[g.edge_label(prev, current)] # Add token to end of list, point to it from old state activeTokens.append(g.edge_label(prev, current)) state_to_active_token[prev] = len(activeTokens) - 1 token_to_states.append([prev]) # Inactivate reverse token, find new token for its states # # (the 'active' token of 'current' is necessarily the label of # (current, previous)) activeTokens[state_to_active_token[current]] = None for v in token_to_states[state_to_active_token[current]]: if v != current: try: scan(v) except StopIteration: return fail # All checks passed, return the result if certificate: format = "{0:0%db}" % dim return (True, {v: format.format(l) for v, l in embed.items()}) else: return True
def RandomBlockGraph(m, k, kmax=None, incidence_structure=False): r""" Return a Random Block Graph. A block graph is a connected graph in which every biconnected component (block) is a clique. .. SEEALSO:: - :wikipedia:`Block_graph` for more details on these graphs - :meth:`~sage.graphs.graph.Graph.is_block_graph` -- test if a graph is a block graph - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree` - :meth:`~sage.combinat.designs.incidence_structures.IncidenceStructure` INPUT: - ``m`` -- integer; number of blocks (at least one). - ``k`` -- integer; minimum number of vertices of a block (at least two). - ``kmax`` -- integer (default: ``None``) By default, each block has `k` vertices. When the parameter `kmax` is specified (with `kmax \geq k`), the number of vertices of each block is randomly chosen between `k` and `kmax`. - ``incidence_structure`` -- boolean (default: ``False``) when set to ``True``, the incidence structure of the graphs is returned instead of the graph itself, that is the list of the lists of vertices in each block. This is useful for the creation of some hypergraphs. OUTPUT: A Graph when ``incidence_structure==False`` (default), and otherwise an incidence structure. EXAMPLES: A block graph with a single block is a clique:: sage: B = graphs.RandomBlockGraph(1, 4) sage: B.is_clique() True A block graph with blocks of order 2 is a tree:: sage: B = graphs.RandomBlockGraph(10, 2) sage: B.is_tree() True Every biconnected component of a block graph is a clique:: sage: B = graphs.RandomBlockGraph(5, 3, kmax=6) sage: blocks,cuts = B.blocks_and_cut_vertices() sage: all(B.is_clique(block) for block in blocks) True A block graph with blocks of order `k` has `m*(k-1)+1` vertices:: sage: m, k = 6, 4 sage: B = graphs.RandomBlockGraph(m, k) sage: B.order() == m*(k-1)+1 True Test recognition methods:: sage: B = graphs.RandomBlockGraph(6, 2, kmax=6) sage: B.is_block_graph() True sage: B in graph_classes.Block True Asking for the incidence structure:: sage: m, k = 6, 4 sage: IS = graphs.RandomBlockGraph(m, k, incidence_structure=True) sage: from sage.combinat.designs.incidence_structures import IncidenceStructure sage: IncidenceStructure(IS) Incidence structure with 19 points and 6 blocks sage: m*(k-1)+1 19 TESTS: A block graph has at least one block, so `m\geq 1`:: sage: B = graphs.RandomBlockGraph(0, 1) Traceback (most recent call last): ... ValueError: the number `m` of blocks must be >= 1 A block has at least 2 vertices, so `k\geq 2`:: sage: B = graphs.RandomBlockGraph(1, 1) Traceback (most recent call last): ... ValueError: the minimum number `k` of vertices in a block must be >= 2 The maximum size of a block is at least its minimum size, so `k\leq kmax`:: sage: B = graphs.RandomBlockGraph(1, 3, kmax=2) Traceback (most recent call last): ... ValueError: the maximum number `kmax` of vertices in a block must be >= `k` """ import itertools from sage.misc.prandom import choice from sage.sets.disjoint_set import DisjointSet if m < 1: raise ValueError("the number `m` of blocks must be >= 1") if k < 2: raise ValueError( "the minimum number `k` of vertices in a block must be >= 2") if kmax is None: kmax = k elif kmax < k: raise ValueError( "the maximum number `kmax` of vertices in a block must be >= `k`") if m == 1: # A block graph with a single block is a clique IS = [list(range(randint(k, kmax)))] elif kmax == 2: # A block graph with blocks of order 2 is a tree IS = [list(e) for e in RandomTree(m + 1).edges(labels=False)] else: # We start with a random tree of order m T = RandomTree(m) # We create a block of order in range [k,kmax] per vertex of the tree B = {u: [(u, i) for i in range(randint(k, kmax))] for u in T} # For each edge of the tree, we choose 1 vertex in each of the # corresponding blocks and we merge them. We use a disjoint set data # structure to keep a unique identifier per merged vertices DS = DisjointSet([i for u in B for i in B[u]]) for u, v in T.edges(labels=0): DS.union(choice(B[u]), choice(B[v])) # We relabel vertices in the range [0, m*(k-1)] and build the incidence # structure new_label = { root: i for i, root in enumerate(DS.root_to_elements_dict()) } IS = [[new_label[DS.find(v)] for v in B[u]] for u in B] if incidence_structure: return IS # We finally build the block graph if k == kmax: BG = Graph( name="Random Block Graph with {} blocks of order {}".format(m, k)) else: BG = Graph( name="Random Block Graph with {} blocks of order {} to {}".format( m, k, kmax)) for block in IS: BG.add_clique(block) return BG
def RandomBlockGraph(m, k, kmax=None, incidence_structure=False): r""" Return a Random Block Graph. A block graph is a connected graph in which every biconnected component (block) is a clique. .. SEEALSO:: - :wikipedia:`Block_graph` for more details on these graphs - :meth:`~sage.graphs.graph.Graph.is_block_graph` -- test if a graph is a block graph - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree` - :meth:`~sage.combinat.designs.incidence_structures.IncidenceStructure` INPUT: - ``m`` -- integer; number of blocks (at least one). - ``k`` -- integer; minimum number of vertices of a block (at least two). - ``kmax`` -- integer (default: ``None``) By default, each block has `k` vertices. When the parameter `kmax` is specified (with `kmax \geq k`), the number of vertices of each block is randomly chosen between `k` and `kmax`. - ``incidence_structure`` -- boolean (default: ``False``) when set to ``True``, the incidence structure of the graphs is returned instead of the graph itself, that is the list of the lists of vertices in each block. This is useful for the creation of some hypergraphs. OUTPUT: A Graph when ``incidence_structure==False`` (default), and otherwise an incidence structure. EXAMPLES: A block graph with a single block is a clique:: sage: B = graphs.RandomBlockGraph(1, 4) sage: B.is_clique() True A block graph with blocks of order 2 is a tree:: sage: B = graphs.RandomBlockGraph(10, 2) sage: B.is_tree() True Every biconnected component of a block graph is a clique:: sage: B = graphs.RandomBlockGraph(5, 3, kmax=6) sage: blocks,cuts = B.blocks_and_cut_vertices() sage: all(B.is_clique(block) for block in blocks) True A block graph with blocks of order `k` has `m*(k-1)+1` vertices:: sage: m, k = 6, 4 sage: B = graphs.RandomBlockGraph(m, k) sage: B.order() == m*(k-1)+1 True Test recognition methods:: sage: B = graphs.RandomBlockGraph(6, 2, kmax=6) sage: B.is_block_graph() True sage: B in graph_classes.Block True Asking for the incidence structure:: sage: m, k = 6, 4 sage: IS = graphs.RandomBlockGraph(m, k, incidence_structure=True) sage: from sage.combinat.designs.incidence_structures import IncidenceStructure sage: IncidenceStructure(IS) Incidence structure with 19 points and 6 blocks sage: m*(k-1)+1 19 TESTS: A block graph has at least one block, so `m\geq 1`:: sage: B = graphs.RandomBlockGraph(0, 1) Traceback (most recent call last): ... ValueError: the number `m` of blocks must be >= 1 A block has at least 2 vertices, so `k\geq 2`:: sage: B = graphs.RandomBlockGraph(1, 1) Traceback (most recent call last): ... ValueError: the minimum number `k` of vertices in a block must be >= 2 The maximum size of a block is at least its minimum size, so `k\leq kmax`:: sage: B = graphs.RandomBlockGraph(1, 3, kmax=2) Traceback (most recent call last): ... ValueError: the maximum number `kmax` of vertices in a block must be >= `k` """ import itertools from sage.misc.prandom import choice from sage.sets.disjoint_set import DisjointSet if m < 1: raise ValueError("the number `m` of blocks must be >= 1") if k < 2: raise ValueError("the minimum number `k` of vertices in a block must be >= 2") if kmax is None: kmax = k elif kmax < k: raise ValueError("the maximum number `kmax` of vertices in a block must be >= `k`") if m == 1: # A block graph with a single block is a clique IS = [ list(range(randint(k, kmax))) ] elif kmax == 2: # A block graph with blocks of order 2 is a tree IS = [ list(e) for e in RandomTree(m+1).edges(labels=False) ] else: # We start with a random tree of order m T = RandomTree(m) # We create a block of order in range [k,kmax] per vertex of the tree B = {u:[(u,i) for i in range(randint(k, kmax))] for u in T} # For each edge of the tree, we choose 1 vertex in each of the # corresponding blocks and we merge them. We use a disjoint set data # structure to keep a unique identifier per merged vertices DS = DisjointSet([i for u in B for i in B[u]]) for u,v in T.edges(labels=0): DS.union(choice(B[u]), choice(B[v])) # We relabel vertices in the range [0, m*(k-1)] and build the incidence # structure new_label = {root:i for i,root in enumerate(DS.root_to_elements_dict())} IS = [ [new_label[DS.find(v)] for v in B[u]] for u in B ] if incidence_structure: return IS # We finally build the block graph if k == kmax: BG = Graph(name = "Random Block Graph with {} blocks of order {}".format(m, k)) else: BG = Graph(name = "Random Block Graph with {} blocks of order {} to {}".format(m, k, kmax)) for block in IS: BG.add_clique( block ) return BG