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
}
}