Files
2026-09-21 12:15:57 +03:00

722 lines
22 KiB
C#

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