Преобразование направленного ациклического графа (DAG) в дерево
Я пытаюсь реализовать алгоритм преобразования направленного ациклического графа в дерево (для развлечения, обучения, ката, назовите его). Итак, я придумаю структуру данных Node:
/// <summary>
/// Represeting a node in DAG or Tree
/// </summary>
/// <typeparam name="T">Value of the node</typeparam>
public class Node<T>
{
/// <summary>
/// creats a node with no child nodes
/// </summary>
/// <param name="value">Value of the node</param>
public Node(T value)
{
Value = value;
ChildNodes = new List<Node<T>>();
}
/// <summary>
/// Creates a node with given value and copy the collection of child nodes
/// </summary>
/// <param name="value">value of the node</param>
/// <param name="childNodes">collection of child nodes</param>
public Node(T value, IEnumerable<Node<T>> childNodes)
{
if (childNodes == null)
{
throw new ArgumentNullException("childNodes");
}
ChildNodes = new List<Node<T>>(childNodes);
Value = value;
}
/// <summary>
/// Determines if the node has any child node
/// </summary>
/// <returns>true if has any</returns>
public bool HasChildNodes
{
get { return this.ChildNodes.Count != 0; }
}
/// <summary>
/// Travearse the Graph recursively
/// </summary>
/// <param name="root">root node</param>
/// <param name="visitor">visitor for each node</param>
public void Traverse(Node<T> root, Action<Node<T>> visitor)
{
if (root == null)
{
throw new ArgumentNullException("root");
}
if (visitor == null)
{
throw new ArgumentNullException("visitor");
}
visitor(root);
foreach (var node in root.ChildNodes)
{
Traverse(node, visitor);
}
}
/// <summary>
/// Value of the node
/// </summary>
public T Value { get; private set; }
/// <summary>
/// List of all child nodes
/// </summary>
public List<Node<T>> ChildNodes { get; private set; }
}
Это довольно просто. Методы:
/// <summary>
/// Helper class for Node
/// </summary>
/// <typeparam name="T">Value of a node</typeparam>
public static class NodeHelper
{
/// <summary>
/// Converts Directed Acyclic Graph to Tree data structure using recursion.
/// </summary>
/// <param name="root">root of DAG</param>
/// <param name="seenNodes">keep track of child elements to find multiple connections (f.e. A connects with B and C and B also connects with C)</param>
/// <returns>root node of the tree</returns>
public static Node<T> DAG2TreeRec<T>(this Node<T> root, HashSet<Node<T>> seenNodes)
{
if (root == null)
{
throw new ArgumentNullException("root");
}
if (seenNodes == null)
{
throw new ArgumentNullException("seenNodes");
}
var length = root.ChildNodes.Count;
for (int i = 0; i < length; ++i)
{
var node = root.ChildNodes[i];
if (seenNodes.Contains(node))
{
var nodeClone = new Node<T>(node.Value, node.ChildNodes);
node = nodeClone;
}
else
{
seenNodes.Add(node);
}
DAG2TreeRec(node, seenNodes);
}
return root;
}
/// <summary>
/// Converts Directed Acyclic Graph to Tree data structure using explicite stack.
/// </summary>
/// <param name="root">root of DAG</param>
/// <param name="seenNodes">keep track of child elements to find multiple connections (f.e. A connects with B and C and B also connects with C)</param>
/// <returns>root node of the tree</returns>
public static Node<T> DAG2Tree<T>(this Node<T> root, HashSet<Node<T>> seenNodes)
{
if (root == null)
{
throw new ArgumentNullException("root");
}
if (seenNodes == null)
{
throw new ArgumentNullException("seenNodes");
}
var stack = new Stack<Node<T>>();
stack.Push(root);
while (stack.Count > 0)
{
var tempNode = stack.Pop();
var length = tempNode.ChildNodes.Count;
for (int i = 0; i < length; ++i)
{
var node = tempNode.ChildNodes[i];
if (seenNodes.Contains(node))
{
var nodeClone = new Node<T>(node.Value, node.ChildNodes);
node = nodeClone;
}
else
{
seenNodes.Add(node);
}
stack.Push(node);
}
}
return root;
}
}
и проверить:
static void Main(string[] args)
{
// Jitter preheat
Dag2TreeTest();
Dag2TreeRecTest();
Console.WriteLine("Running time ");
Dag2TreeTest();
Dag2TreeRecTest();
Console.ReadKey();
}
public static void Dag2TreeTest()
{
HashSet<Node<int>> hashSet = new HashSet<Node<int>>();
Node<int> root = BulidDummyDAG();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var treeNode = root.DAG2Tree<int>(hashSet);
stopwatch.Stop();
Console.WriteLine(string.Format("Dag 2 Tree = {0}ms",stopwatch.ElapsedMilliseconds));
}
private static Node<int> BulidDummyDAG()
{
Node<int> node2 = new Node<int>(2);
Node<int> node4 = new Node<int>(4);
Node<int> node3 = new Node<int>(3);
Node<int> node5 = new Node<int>(5);
Node<int> node6 = new Node<int>(6);
Node<int> node7 = new Node<int>(7);
Node<int> node8 = new Node<int>(8);
Node<int> node9 = new Node<int>(9);
Node<int> node10 = new Node<int>(10);
Node<int> root = new Node<int>(1);
//making DAG
root.ChildNodes.Add(node2);
root.ChildNodes.Add(node3);
node3.ChildNodes.Add(node2);
node3.ChildNodes.Add(node4);
root.ChildNodes.Add(node5);
node4.ChildNodes.Add(node6);
node4.ChildNodes.Add(node7);
node5.ChildNodes.Add(node8);
node2.ChildNodes.Add(node9);
node9.ChildNodes.Add(node8);
node9.ChildNodes.Add(node10);
var length = 10000;
Node<int> tempRoot = node10;
for (int i = 0; i < length; i++)
{
var nextChildNode = new Node<int>(11 + i);
tempRoot.ChildNodes.Add(nextChildNode);
tempRoot = nextChildNode;
}
return root;
}
public static void Dag2TreeRecTest()
{
HashSet<Node<int>> hashSet = new HashSet<Node<int>>();
Node<int> root = BulidDummyDAG();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var treeNode = root.DAG2TreeRec<int>(hashSet);
stopwatch.Stop();
Console.WriteLine(string.Format("Dag 2 Tree Rec = {0}ms",stopwatch.ElapsedMilliseconds));
}
Более того, структура данных нуждается в некотором улучшении:
- Переопределение оператора GetHash, toString, Equals, ==
- реализация IComparable
- LinkedList, вероятно, лучший выбор
Кроме того, перед конвертацией необходимо проверить некоторые чертины:
- мультиграфы
- Если это DAG (циклы)
- Diamnods в DAG
- Несколько корней в DAG
В целом, это сводится к нескольким вопросам:как я могу улучшить конверсию? Поскольку это рекурион, возможно взорвать стек. Я могу добавить стек, чтобы запомнить его. Если я сделаю стиль прохождения продолжения, буду ли я более эффективным?
Я чувствую, что неизменная структура данных в этом случае будет лучше. Это правильно?
Чайлдс - правильное имя?:)
2 ответа
Алгоритм:
Как вы заметили, некоторые узлы появляются дважды в выводе. Если бы у узла 2 были дочерние элементы, все поддерево появилось бы дважды. Если вы хотите, чтобы каждый узел появлялся только один раз, замените
if (hashSet.Contains(node)) { var nodeClone = new Node<T>(node.Value, node.Childs); node = nodeClone; }
с
if (hashSet.Contains(node)) { // node already seen -> do nothing }
Я не буду слишком беспокоиться о размере стека или производительности рекурсии. Однако вы можете заменить поиск в глубину первым на поиск в ширину, что приведет к тому, что узлы будут ближе к корню, которые были посещены ранее, что приведет к более "естественному" дереву (на вашем рисунке вы уже пронумеровали узлы в порядке BFS).
var seenNodes = new HashSet<Node>(); var q = new Queue<Node>(); q.Enqueue(root); seenNodes.Add(root); while (q.Count > 0) { var node = q.Dequeue(); foreach (var child in node.Childs) { if (!seenNodes.Contains(child )) { seenNodes.Add(child); q.Enqueue(child); } }
Алгоритм обрабатывает алмазы и циклы.
Несколько корней
Просто объявите класс Graph, который будет содержать все вершины
class Graph { public List<Node> Nodes { get; private set; } public Graph() { Nodes = new List<Node>(); } }
Код:
hashSet может быть назван seenNodes.
Вместо
var length = root.Childs.Count; for (int i = 0; i < length; ++i) { var node = root.Childs[i];
записывать
foreach (var child in root.Childs)
В Траверсе посетитель совершенно не нужен. Вы могли бы иметь метод, который возвращает все узлы дерева (в том же порядке, что и ход), и пользователь может делать с узлами все, что угодно:
foreach(var node in root.TraverseRecursive()) { Console.WriteLine(node.Value); }
Если вы переопределите GetHashCode и Equals, алгоритм больше не сможет различать два разных узла с одинаковым значением, что, вероятно, не то, что вам нужно.
Я не вижу никакой причины, почему LinkedList был бы лучше, чем List, за исключением перераспределений (Capacity 2,4,8,16,...), которые List делает при добавлении узлов.
- Вы лучше разместили в CodeReview
- Чайлдс не прав => Дети
вам не нужно использовать HashSet, вы могли бы легко использовать List>, потому что здесь достаточно только проверки ссылок. (и поэтому не требуется переопределение GetHashCode, Equals и операторов)
Более простой способ - сериализация вашего класса и последующая десериализация его во второй объект с XmlSerializer. при сериализации и десериализации 1 объект, на который ссылаются 2 раза, станет 2 объектами с разными ссылками.