using System; using System.Collections.Generic; using System.Linq; namespace AutoGroupGenerator { /// /// Generic directed graph with traversal and analysis helpers. /// /// The node value type stored in the graph. [Serializable] public class Graph where T : IEquatable { #region Types /// /// Serializable representation of a node and its neighbors. /// [Serializable] public class SerializableNode { /// /// The node value. /// public T Node; /// /// The neighboring node values. /// public List Neighbors = new(); } /// /// Serializable payload for an entire graph. /// [Serializable] public class SerializableData { /// /// Collection of serialized nodes. /// public List Nodes = new(); } #endregion #region Fields /// /// Adjacency list mapping nodes to outgoing neighbors. /// [NonSerialized] protected Dictionary> _adjacencyList; #endregion #region Properties /// /// Gets the number of nodes in the graph. /// public int NodeCount => _adjacencyList.Count; #endregion #region Methods /// /// Initializes an empty graph. /// public Graph() { _adjacencyList = new Dictionary>(); } /// /// Adds an edge between two nodes, adding missing nodes as needed. /// /// The source node. /// The destination node. public virtual void AddEdge(T fromNode, T toNode) { AddNode(fromNode); AddNode(toNode); _adjacencyList[fromNode].Add(toNode); } /// /// Adds a node to the graph if it does not already exist. /// /// The node to add. public virtual void AddNode(T node) { if (!_adjacencyList.ContainsKey(node)) { _adjacencyList[node] = new List(); } } /// /// Gets the neighbors for the specified node. /// /// The node to query. /// A list of neighboring nodes. public List GetNeighbors(T node) { return _adjacencyList.TryGetValue(node, out List neighbors) ? neighbors : new List(); } /// /// Gets all nodes in the graph. /// /// A list of all nodes. public List GetAllNodes() { return new List(_adjacencyList.Keys); } /// /// Builds the transposed graph with all edges reversed. /// /// The transposed graph. public Graph GetTransposedGraph() { var transposed = new Graph(); foreach (var node in _adjacencyList.Keys) { transposed.AddNode(node); } foreach (var from in _adjacencyList) { foreach (var to in from.Value) { transposed.AddEdge(to, from.Key); } } return transposed; } /// /// Serializes the graph into a data transfer object. /// /// The serializable data. public SerializableData ToSerializableData() { var data = new SerializableData(); foreach (var kvp in _adjacencyList) { data.Nodes.Add(new SerializableNode { Node = kvp.Key, Neighbors = new List(kvp.Value) }); } return data; } /// /// Rehydrates a graph from serialized data. /// /// The serialized graph data. /// The reconstructed graph. public static Graph FromSerializableData(SerializableData data) { var graph = new Graph(); graph._adjacencyList.Clear(); foreach (var serializedNode in data.Nodes) { graph._adjacencyList[serializedNode.Node] = serializedNode.Neighbors != null ? new List(serializedNode.Neighbors) : new List(); } return graph; } /// /// Finds all leaf nodes reachable from the given node. /// /// The node to start from. /// The set of reachable leaf nodes. public HashSet FindLeafNodes(T node) { var visited = new HashSet(); var roots = new HashSet(); void Dfs(T current) { if (visited.Contains(current)) { return; } visited.Add(current); var neighbors = GetNeighbors(current); if (neighbors == null || neighbors.Count == 0) { roots.Add(current); return; } foreach (var neighbor in neighbors) { Dfs(neighbor); } } Dfs(node); return roots; } /// /// Finds every node along paths from the starting node to leaves. /// /// The node to start from. /// All nodes encountered along paths to leaves. public HashSet FindPathConeToLeaves(T node) { var visited = new HashSet(); var result = new HashSet(); void Dfs(T current) { if (visited.Contains(current)) { return; } visited.Add(current); result.Add(current); var neighbors = GetNeighbors(current); if (neighbors == null || neighbors.Count == 0) { return; } foreach (var neighbor in neighbors) { Dfs(neighbor); } } Dfs(node); return result; } /// /// Finds nodes along paths from the starting node and the leaf nodes reached. /// /// The node to start from. /// Outputs the nodes along the traversed paths. /// Outputs the leaf nodes that were reached. public void FindPathAndLeaves(T node, out HashSet path, out HashSet leaves) { path = new HashSet(); leaves = new HashSet(); var visited = new HashSet(); FindPathAndLeavesRecursive(node, path, leaves, visited); } private void FindPathAndLeavesRecursive(T current, HashSet path, HashSet leaves, HashSet visited) { if (visited.Contains(current)) { return; } visited.Add(current); path.Add(current); var neighbors = GetNeighbors(current); if (neighbors == null || neighbors.Count == 0) { leaves.Add(current); return; } foreach (var neighbor in neighbors) { FindPathAndLeavesRecursive(neighbor, path, leaves, visited); } } /// /// Finds all paths from the start node to any leaf node. /// /// The node to start from. /// A list of paths, where each path is a list of nodes. public List> FindAllPathsToLeafNodes(T startNode) { var allPaths = new List>(); Stack>> stack = new Stack>>(); stack.Push(Tuple.Create(startNode, new List { startNode })); while (stack.Count > 0) { var (currentNode, currentPath) = stack.Pop(); var neighbors = GetNeighbors(currentNode); if (neighbors.Count == 0) { allPaths.Add(new List(currentPath)); continue; } foreach (var neighbor in neighbors) { if (!currentPath.Contains(neighbor)) // Avoid revisiting nodes in the current path { List newPath = new List(currentPath) { neighbor }; stack.Push(Tuple.Create(neighbor, newPath)); } else { allPaths.Add(new List(currentPath)); } } } return allPaths; } /// /// Performs a depth-first traversal starting at the given node. /// /// The node to start from. /// Callback invoked when a node is visited. public void DepthFirstSearch(T startNode, Action onVisit) { var visited = new HashSet(); DepthFirstSearchIterative(startNode, visited, onVisit); } /// /// Performs a depth-first traversal with a provided visited set. /// /// The node to start from. /// Set of already-visited nodes. /// Callback invoked when a node is visited. public void DepthFirstSearchIterative(T startNode, HashSet visited, Action onVisit) { var stack = new Stack(); stack.Push(startNode); while (stack.Count > 0) { T node = stack.Pop(); if (!visited.Contains(node)) { visited.Add(node); onVisit?.Invoke(node); foreach (var neighbor in GetNeighbors(node)) { if (!visited.Contains(neighbor)) { stack.Push(neighbor); } } } } } /// /// Finds all paths between a start and end node using DFS. /// /// The node to start from. /// The node to end at. /// A list of paths between the nodes. public List> DepthFirstSearchForAllPaths(T startNode, T endNode) { var allPaths = new List>(); var stack = new Stack>>(); stack.Push(Tuple.Create(startNode, new List { startNode })); while (stack.Count > 0) { var (currentNode, currentPath) = stack.Pop(); if (EqualityComparer.Default.Equals(currentNode, endNode)) { allPaths.Add(new List(currentPath)); } foreach (var neighbor in GetNeighbors(currentNode)) { if (!currentPath.Contains(neighbor)) { var newPath = new List(currentPath) { neighbor }; stack.Push(Tuple.Create(neighbor, newPath)); } } } return allPaths; } /// /// Finds all paths from a start node to nodes matching a condition. /// /// The node to start from. /// Predicate identifying acceptable end nodes. /// A list of matching paths. public List> DepthFirstSearchForAllPaths(T startNode, Predicate endNodeCondition) { var allPaths = new List>(); var stack = new Stack>>(); stack.Push(Tuple.Create(startNode, new List { startNode })); while (stack.Count > 0) { var (currentNode, currentPath) = stack.Pop(); if (endNodeCondition(currentNode)) { allPaths.Add(new List(currentPath)); } foreach (var neighbor in GetNeighbors(currentNode)) { if (!currentPath.Contains(neighbor)) { var newPath = new List(currentPath) { neighbor }; stack.Push(Tuple.Create(neighbor, newPath)); } else { allPaths.Add(new List(currentPath)); } } } return allPaths; } /// /// Performs a breadth-first traversal starting at the given node. /// /// The node to start from. /// Callback invoked when a node is visited. public void BreadthFirstSearch(T startNode, Action onVisit) { var visited = new HashSet(); var queue = new Queue(); visited.Add(startNode); queue.Enqueue(startNode); while (queue.Count > 0) { T node = queue.Dequeue(); onVisit?.Invoke(node); foreach (var neighbor in GetNeighbors(node)) { if (!visited.Contains(neighbor)) { visited.Add(neighbor); queue.Enqueue(neighbor); } } } } /// /// Converts the graph to use a different node type. /// /// The destination node type. /// Converter from the source node type to the destination type. /// A graph with converted node values. public Graph ConvertNodeType(Func converter) where TResult : IEquatable { var destinationAdjacencyList = new Dictionary>(); foreach (var node in _adjacencyList) { TResult newNode = converter(node.Key); var neighbors = new List(); foreach (var neighbor in node.Value) { neighbors.Add(converter(neighbor)); } destinationAdjacencyList.Add(newNode, neighbors); } return new Graph { _adjacencyList = destinationAdjacencyList }; } /// /// Creates a subgraph containing only the specified nodes. /// /// Nodes to include in the subgraph. /// The resulting subgraph. public Graph GetSubgraph(List subgraphNodes) { var subgraphAdjacencyList = new Dictionary>(); foreach (T node in subgraphNodes) { if (_adjacencyList.TryGetValue(node, out var sourceNeighbors)) { List neighbors = sourceNeighbors.Where(subgraphNodes.Contains).ToList(); subgraphAdjacencyList.Add(node, neighbors); } } return new Graph { _adjacencyList = subgraphAdjacencyList }; } #endregion #region Static Methods /// /// Converts a directed graph into an undirected graph by mirroring edges. /// /// The directed graph to convert. /// An undirected graph. public static Graph ToUndirected(Graph directedGraph) { Graph undirectedGraph = new Graph(); foreach (var node in directedGraph._adjacencyList.Keys) { foreach (var neighbor in directedGraph._adjacencyList[node]) { undirectedGraph.AddEdge(node, neighbor); undirectedGraph.AddEdge(neighbor, node); } } return undirectedGraph; } /// /// Removes a node and its edges from an undirected graph. /// /// The graph to modify. /// The node to remove. public static void RemoveNodeFromUndirectedGraph(Graph undirectedGraph, T targetNode) { var neighbors = undirectedGraph.GetNeighbors(targetNode); if (neighbors.Count > 0) { foreach (var neighbor in neighbors) { var neighborsOfNeighbor = undirectedGraph.GetNeighbors(neighbor); if (neighborsOfNeighbor.Count > 0) { neighborsOfNeighbor.Remove(targetNode); } } } undirectedGraph._adjacencyList.Remove(targetNode); } /// /// Returns the connected components of an undirected graph. /// /// The undirected graph to analyze. /// A list of connected components. public static List> GetConnectedComponentsOfUndirectedGraph(Graph undirectedGraph) { var nodes = undirectedGraph.GetAllNodes(); var visited = new HashSet(); var components = new List>(); foreach (var node in nodes) { if (!visited.Contains(node)) { var component = new List(); undirectedGraph.DepthFirstSearchIterative(node, visited, (currentNode) => component.Add(currentNode)); components.Add(component); } } return components; } /// /// Collapses strongly connected components into a DAG of instances. /// /// A graph whose nodes represent strongly connected components. public Graph CollapseToSCCs() { int index = 0; var indices = new Dictionary(); var lowlinks = new Dictionary(); var onStack = new HashSet(); var stack = new Stack(); var rawSCCs = new List>(); void StrongConnect(T node) { indices[node] = index; lowlinks[node] = index; index++; stack.Push(node); onStack.Add(node); foreach (var neighbor in GetNeighbors(node)) { if (!indices.ContainsKey(neighbor)) { StrongConnect(neighbor); lowlinks[node] = Math.Min(lowlinks[node], lowlinks[neighbor]); } else if (onStack.Contains(neighbor)) { lowlinks[node] = Math.Min(lowlinks[node], indices[neighbor]); } } if (lowlinks[node] == indices[node]) { var scc = new HashSet(); T w; do { w = stack.Pop(); onStack.Remove(w); scc.Add(w); } while (!EqualityComparer.Default.Equals(w, node)); rawSCCs.Add(scc); } } foreach (var node in GetAllNodes()) { if (!indices.ContainsKey(node)) StrongConnect(node); } var nodeToSuper = new Dictionary(); var superNodes = new List(); foreach (var scc in rawSCCs) { var assetNodes = scc.Cast(); // Assumes T is AssetNode var superNode = new SuperNode(assetNodes); superNodes.Add(superNode); foreach (var node in scc) { nodeToSuper[node] = superNode; } } var dag = new Graph(); foreach (var super in superNodes) { dag.AddNode(super); } foreach (var from in _adjacencyList.Keys) { foreach (var to in _adjacencyList[from]) { var fromSuper = nodeToSuper[from]; var toSuper = nodeToSuper[to]; if (!ReferenceEquals(fromSuper, toSuper)) { dag.AddEdge(fromSuper, toSuper); } } } return dag; } #endregion } }