Методы решения циклических зависимостей C#

У меня есть архитектура, которая представляет собой граф, но с иерархией:

public sealed class Vertex : DefaultMethodsMeshComponent, ChildOf<Edge>, ChildOf<Polygon>, ChildOf<ComplexMesh>
{
    
    public LocalVector3 MeshPosition { get; set; }
    public int IdInMesh { get; private set; }

    private List<Edge> parents = new List<Edge>();
    public List<Edge> Parents => parents;
}
public sealed class Edge: DefaultMethodsMeshComponent, ParentOf<Vertex>, ChildOf<Polygon>, ChildOf<ComplexMesh>
{
    private Pair<Vertex> vertices = new();
    public Pair<Vertex> Vertices => vertices;

    private Pair<Polygon> parents = new Pair<Polygon>();
    public Pair<Polygon> Parents => parents;
}
public sealed class Polygon : DefaultMethodsMeshComponent, ParentOf<Edge>, ParentOf<Vertex>, ChildOf<ComplexMesh>
{
    private ComplexMesh> parent;
    public ComplexMesh Parent => parent;
    private List<Edge> edges = new();
    public List<Edge> Edges => edges;
}
public class ComplexMesh : DefaultMethodsMeshComponent, ParentOf<Polygon>, ParentOf<Edge>, ParentOf<Vertex>
{
    private List<Polygon> polygons;
    public List<Polygon> Polygons => polygons;
}

Однако, как видно, архитектура полна циклических зависимостей. Вопрос: можно ли управлять этими зависимостями при удалении объектов более эффективно, чем проходиться по связанным компонентам и удалять ссылки из каждой из них? Для реализации важно, чтобы эти списки были расширяемы, а все вершины представляли собой ссылочные типы во избежание дубликатов с ссылкой на одну и ту же память.


Ответы (1 шт):

Автор решения: tym32167

Вкину альтернативный вариант. Но для начала стоит пояснить, что оптимальный вариант для вас зависит от ваших условий.

Например, от количества ребер в графе и того, как часто узлы должны удаляться. Например, в полном односвязном графе удаление узла из связей потребует N операций, где N - число вершин в графе. Удаление всех вершин займет 0.5*N^2. Для больших графов это может быть очень долго.

Вторая проблема - это написание алгоритмов на графе потребует обхода ваших узлов. Возможно это и не проблема, но, например, я предпочитаю разделать алгоритмы и внутренние структуры моего приложения.

Потому как альтернатиыный вариант предложенному в соседнем ответе, я предложу написать отдельный класс для графов и алгоритмов над ними. Этот отдельный граф будет совершенно отвязан от ваших узлов, н по нему можно также пускать поик с прочие алгоритмы.

Вот пример простейшего графа.

public class Graph<T> where T : notnull
{
    private Dictionary<T, HashSet<T>> adjacencyList;

    public Graph() => adjacencyList = new Dictionary<T, HashSet<T>>();

    public void AddRelation(T source, T destination) => adjacencyList[Ensure(source)].Add(Ensure(destination));

    private T Ensure(T node)
    {
        if (!adjacencyList.ContainsKey(node))
            adjacencyList[node] = new HashSet<T>();
        return node;
    }

    public bool RemoveRelation(T source, T destination) => adjacencyList[source].Remove(destination);

    public bool RemoveNode(T node) => adjacencyList.Remove(node);

    public void Cleanup()
    {
        foreach (var set in adjacencyList.Values)
            set.RemoveWhere(x => !adjacencyList.ContainsKey(x));
    }

    public IEnumerable<T> DFS(T starterNode) => Validate(DFSInternal(starterNode));
    private IEnumerable<T> DFSInternal(T starterNode)
    {
        if (!adjacencyList.ContainsKey(starterNode))
            yield break;

        var visited = new HashSet<T>();
        var stack = new Stack<T>();

        stack.Push(starterNode);

        while (stack.Count > 0)
        {
            var currentNode = stack.Pop();

            if (visited.Contains(currentNode))
                continue;

            visited.Add(currentNode);
            yield return currentNode;

            if (adjacencyList.ContainsKey(currentNode))
            {
                foreach (var neighbor in adjacencyList[currentNode])
                {
                    if (adjacencyList.ContainsKey(neighbor) && !visited.Contains(neighbor))
                        stack.Push(neighbor);
                }
            }
        }
    }

    IEnumerable<T> Validate(IEnumerable<T> nodes)
    {
        foreach (var node in nodes)
            if (adjacencyList.ContainsKey(node)) yield return node;
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.AppendLine("Graph Structure:");

        if (adjacencyList.Count == 0)
        {
            sb.AppendLine("  Empty graph");
            return sb.ToString();
        }

        foreach (var node in adjacencyList.Keys)
        {
            sb.Append($"  Node {node} -> ");

            if (adjacencyList[node].Count == 0)
            {
                sb.AppendLine("No connections");
            }
            else
            {
                sb.AppendLine(string.Join(", ", Validate(adjacencyList[node])));
            }
        }

        return sb.ToString();
    }
}

Как с ним рабтотать

var graph = new Graph<int>();

graph.AddRelation(1, 2);
graph.AddRelation(1, 3);
graph.AddRelation(2, 4);
graph.AddRelation(3, 5);
graph.AddRelation(4, 6);
graph.AddRelation(5, 6);

Console.WriteLine("Initial Graph:");
Console.WriteLine(graph);
Console.WriteLine();

Console.WriteLine("All nodes reachable from node 1:");
var nodesFromOne = graph.DFS(1);
Console.WriteLine(string.Join(", ", nodesFromOne));
Console.WriteLine();

Console.WriteLine("All nodes reachable from node 3:");
var nodesFromThree = graph.DFS(3);
Console.WriteLine(string.Join(", ", nodesFromThree));
Console.WriteLine();

Console.WriteLine("Removing node 3 from the graph...");
graph.RemoveNode(3);
Console.WriteLine();

Console.WriteLine("Graph after removing node 3:");
Console.WriteLine(graph);
Console.WriteLine();

Console.WriteLine("All nodes reachable from node 3 after removal:");
var nodesFromThreeAfterRemoval = graph.DFS(3).ToList();
Console.WriteLine(nodesFromThreeAfterRemoval.Count == 0 ? "None (node 3 is not part of the graph)" : string.Join(", ", nodesFromThreeAfterRemoval));
Console.WriteLine();

Console.WriteLine("All nodes reachable from node 1 after removal of node 3:");
var nodesFromOneAfterRemoval = graph.DFS(1);
Console.WriteLine(string.Join(", ", nodesFromOneAfterRemoval));
Console.WriteLine();

Вывод

Initial Graph:
Graph Structure:
  Node 1 -> 2, 3
  Node 2 -> 4
  Node 3 -> 5
  Node 4 -> 6
  Node 5 -> 6
  Node 6 -> No connections


All nodes reachable from node 1:
1, 3, 5, 6, 2, 4

All nodes reachable from node 3:
3, 5, 6

Removing node 3 from the graph...

Graph after removing node 3:
Graph Structure:
  Node 1 -> 2
  Node 2 -> 4
  Node 4 -> 6
  Node 5 -> 6
  Node 6 -> No connections


All nodes reachable from node 3 after removal:
None (node 3 is not part of the graph)

All nodes reachable from node 1 after removal of node 3:
1, 2, 4, 6

Теперь, имея ваши объекты на руках, вы можете по ним построить граф. Вы можете держать граф в актуальном состоянии, удаляя/добавляя узлы и связи когда это требуется. Можно добавлять любые алгоритмы в граф что вам нравится, можно переделывать граф как хотите - это все никак не скажется на ваши внутренние модели данных. Всё, что вам требуется здесь - это отобразить ваши внутренние модели на ключи для графа. Условно, если у вас модели в списке хранятся - то на индексы в списке. Если у моделей есть айдишник - на этот айдишник. Граф может работать с любыми ключами, которые можно хранить в словаре.

Плюсом, удаление узла из графа здесь происходит за константу, но в самом графе есть небольшое оверхед так как приходится валидировать каждый узел на удаление. Это прсто деталь реализации, можно переписывать как удобно, никаких побочек на вашу бизнес логику это не окажет.

Также все связи происходят здесь в графе, они отвязаны от ваших объектов. Вы можете, подобно соседнему ответу, подписаться на удаление вашего объекта из памяти и также удалить его из графа.

→ Ссылка