C# 如何将一组节点划分为子集,每个子集形成一个有向无环图
在C项目中,我要执行一组测试。每个测试都有它所依赖的自己的测试集合。测试网络需要形成有向无环图DAG 使用符号A->B->C,其中A、B、C表示测试,然后 C取决于B, B取决于A 我已经有了一个算法,可以对测试进行排序,这样我就可以按顺序处理它们,从而尊重所有的依赖关系。也就是说,顺序意味着在对整个图评估测试本身之前,先评估每个测试的依赖关系 我想要的是一个算法,它首先需要一组测试,并且可以将它们划分为单独的DAG图(如果有的话)。每个DAG中的测试不需要订购,因为这可以单独完成。这样做的原因是,我可以将每个独立的DAG作为单独的任务运行,并通过这种方式获得一些效率C# 如何将一组节点划分为子集,每个子集形成一个有向无环图,c#,graph-theory,directed-acyclic-graphs,C#,Graph Theory,Directed Acyclic Graphs,在C项目中,我要执行一组测试。每个测试都有它所依赖的自己的测试集合。测试网络需要形成有向无环图DAG 使用符号A->B->C,其中A、B、C表示测试,然后 C取决于B, B取决于A 我已经有了一个算法,可以对测试进行排序,这样我就可以按顺序处理它们,从而尊重所有的依赖关系。也就是说,顺序意味着在对整个图评估测试本身之前,先评估每个测试的依赖关系 我想要的是一个算法,它首先需要一组测试,并且可以将它们划分为单独的DAG图(如果有的话)。每个DAG中的测试不需要订购,因为这可以单独完成。这样做的原因
所以,考虑一组测试A、B、C、D、E、F,它们的依赖关系是:
A -> B -> C
D -> C
E -> F
根据算法,我想做两组测试
Set 1) A,B,C,D
Set 2) E,F
更新:C代码以帮助请求Eric
public class Graph
{
private List<Node> _nodes = new List<Node>();
public IReadOnlyList<Node> Nodes => _nodes;
public void AddNode(Node node)
{
_nodes.Add(node);
}
public void RemoveRange(IEnumerable<Node> nodes)
{
foreach (var item in nodes)
{
_nodes.Remove(item);
}
}
}
public class Node
{
public Node(string name)
{
Name = name;
}
private List<Node> _dependants = new List<Node>();
public string Name { get; private set; }
public IReadOnlyList<Node> Dependents => _dependants;
public void AddDependent(Node node)
{
_dependants.Add(node);
}
}
public class Set
{
private List<Node> _elements = new List<Node>();
public void AddRange(IEnumerable<Node> nodes)
{
_elements = new List<Node>(nodes);
}
public IReadOnlyList<Node> Elements => _elements;
}
internal class Program
{
private static void Main(string[] args)
{
List<Set> sets = new List<Set>();
var graph = new Graph();
var a = new Node("A");
var b = new Node("B");
var c = new Node("C");
var d = new Node("D");
var e = new Node("E");
var f = new Node("F");
graph.AddNode(a);
graph.AddNode(b);
graph.AddNode(c);
graph.AddNode(d);
graph.AddNode(e);
graph.AddNode(f);
c.AddDependent(b);
b.AddDependent(a);
c.AddDependent(d);
f.AddDependent(e);
while (graph.Nodes.Count > 0)
{
var set = new Set();
var pickNode = graph.Nodes[0];
// Get reachable nodes
// 1. NOT SURE WHAT YOU MEAN HERE AND HOW TO DO THIS IN C#
// 2. ALSO, DOES THE SET INCLUDE THE PICKED NODE?
}
}
}
更新2:
排序节点的代码示例
private enum MarkType
{
None,
Permanent,
Temporary
}
private static IEnumerable<T> GetSortedNodes<T>(DirectedGraph<T> directedGraph)
{
List<T> L = new List<T>();
var allNodes = directedGraph.Nodes();
Dictionary<T, (MarkType, T)> nodePairDictionary = allNodes.ToDictionary(n => n, n => (MarkType.None, n));
foreach (var node in allNodes)
{
var nodePair = nodePairDictionary[node];
Visit(nodePair);
}
return L.Reverse<T>().ToList();
void Visit((MarkType markType, T node) nodePair)
{
if (nodePair.markType == MarkType.Permanent)
{
return;
}
if (nodePair.markType == MarkType.Temporary)
{
throw new Exception("NOT A DAG");
}
nodePair.markType = MarkType.Temporary;
foreach (var dependentNode in directedGraph.Edges(nodePair.node))
{
var depNodePair = nodePairDictionary[dependentNode];
Visit(depNodePair);
}
nodePair.markType = MarkType.Permanent;
L.Insert(0, nodePair.node);
}
}
在伪代码中,这样的算法可能如下所示:
Create a list of buckets
foreach (Node n in Nodes)
{
Find a bucket that contains n, or create a new bucket for it.
foreach (Node dependentNode in n.DependentNodes)
{
if (dependentNode is in any bucket)
{
move n and its dependencies to that bucket;
}
else
{
add depenentNode to the same bucket as N;
}
}
}
在遍历所有节点之后,bucket现在应该表示不同的集合,而不存在相互依赖关系
注意:我强烈怀疑这不是最有效的算法。但对于数量有限的节点,它应该足够了
一如既往,我建议提供大量的单元测试以确保正确性,并在出现性能问题时进行分析
下面是一个最小的实现,作为您的示例的单元测试:
[TestFixture]
public class PartitionTests
{
public class Node
{
private List<Node> subNodes = new List<Node>();
public Node(string name)
{
this.Name = name;
}
public IEnumerable<Node> DependentNodes { get { return this.subNodes; } }
public string Name { get; }
internal void AddDependentNode(Node subNode)
{
subNodes.Add(subNode);
}
public override string ToString()
{
//just to make debugging easier in this example
return this.Name;
}
}
[Test]
public void PartitionTest1()
{
#region prepare
Node A = new Node("A");
Node B = new Node("B");
Node C = new Node("C");
Node D = new Node("D");
Node E = new Node("E");
Node F = new Node("F");
A.AddDependentNode(B);
B.AddDependentNode(C);
D.AddDependentNode(C);
E.AddDependentNode(F);
var allNodes = new List<Node>() { A, B, C, D, E, F };
#endregion
#region Implementation
var buckets = new List<List<Node>>();
foreach (var n in allNodes)
{
var existingBucket = buckets.FirstOrDefault(b => b.Contains(n));
if (existingBucket == null)
{
existingBucket = new List<Node>() { n };
}
foreach (var dependentNode in n.DependentNodes)
{
var otherBucket = buckets.FirstOrDefault(b => b.Contains(dependentNode));
if (otherBucket == null)
{
existingBucket.Add(dependentNode);
}
else
{
existingBucket.Remove(n);
otherBucket.Add(n);
foreach (var alreadyPlacedNode in existingBucket)
{
existingBucket.Remove(alreadyPlacedNode);
if (!otherBucket.Contains(alreadyPlacedNode))
{
otherBucket.Add(alreadyPlacedNode);
}
}
}
}
if (!buckets.Contains(existingBucket) && existingBucket.Any())
{
buckets.Add(existingBucket);
}
}
#endregion
#region test
Assert.AreEqual(2, buckets.Count, "Expect two buckets");
Assert.AreEqual(4, buckets[0].Count); //we should not rely on the order of buckets here
Assert.AreEqual(2, buckets[1].Count);
CollectionAssert.Contains(buckets[0], A);
CollectionAssert.Contains(buckets[0], B);
CollectionAssert.Contains(buckets[0], C);
CollectionAssert.Contains(buckets[0], D);
CollectionAssert.Contains(buckets[1], E);
CollectionAssert.Contains(buckets[1], F);
#endregion
}
}
在伪代码中,这样的算法可能如下所示:
Create a list of buckets
foreach (Node n in Nodes)
{
Find a bucket that contains n, or create a new bucket for it.
foreach (Node dependentNode in n.DependentNodes)
{
if (dependentNode is in any bucket)
{
move n and its dependencies to that bucket;
}
else
{
add depenentNode to the same bucket as N;
}
}
}
在遍历所有节点之后,bucket现在应该表示不同的集合,而不存在相互依赖关系
注意:我强烈怀疑这不是最有效的算法。但对于数量有限的节点,它应该足够了
一如既往,我建议提供大量的单元测试以确保正确性,并在出现性能问题时进行分析
下面是一个最小的实现,作为您的示例的单元测试:
[TestFixture]
public class PartitionTests
{
public class Node
{
private List<Node> subNodes = new List<Node>();
public Node(string name)
{
this.Name = name;
}
public IEnumerable<Node> DependentNodes { get { return this.subNodes; } }
public string Name { get; }
internal void AddDependentNode(Node subNode)
{
subNodes.Add(subNode);
}
public override string ToString()
{
//just to make debugging easier in this example
return this.Name;
}
}
[Test]
public void PartitionTest1()
{
#region prepare
Node A = new Node("A");
Node B = new Node("B");
Node C = new Node("C");
Node D = new Node("D");
Node E = new Node("E");
Node F = new Node("F");
A.AddDependentNode(B);
B.AddDependentNode(C);
D.AddDependentNode(C);
E.AddDependentNode(F);
var allNodes = new List<Node>() { A, B, C, D, E, F };
#endregion
#region Implementation
var buckets = new List<List<Node>>();
foreach (var n in allNodes)
{
var existingBucket = buckets.FirstOrDefault(b => b.Contains(n));
if (existingBucket == null)
{
existingBucket = new List<Node>() { n };
}
foreach (var dependentNode in n.DependentNodes)
{
var otherBucket = buckets.FirstOrDefault(b => b.Contains(dependentNode));
if (otherBucket == null)
{
existingBucket.Add(dependentNode);
}
else
{
existingBucket.Remove(n);
otherBucket.Add(n);
foreach (var alreadyPlacedNode in existingBucket)
{
existingBucket.Remove(alreadyPlacedNode);
if (!otherBucket.Contains(alreadyPlacedNode))
{
otherBucket.Add(alreadyPlacedNode);
}
}
}
}
if (!buckets.Contains(existingBucket) && existingBucket.Any())
{
buckets.Add(existingBucket);
}
}
#endregion
#region test
Assert.AreEqual(2, buckets.Count, "Expect two buckets");
Assert.AreEqual(4, buckets[0].Count); //we should not rely on the order of buckets here
Assert.AreEqual(2, buckets[1].Count);
CollectionAssert.Contains(buckets[0], A);
CollectionAssert.Contains(buckets[0], B);
CollectionAssert.Contains(buckets[0], C);
CollectionAssert.Contains(buckets[0], D);
CollectionAssert.Contains(buckets[1], E);
CollectionAssert.Contains(buckets[1], F);
#endregion
}
}
pniederh的答案给出了联合查找算法的一个过于复杂的版本;正如我在一篇评论中所指出的,关于如何使这些算法有效,有很多研究 在您的特定情况下,另一个有效的算法是: 创建表示无向图的类型。 向表示每个任务的图中添加一个节点。 向图中添加一条边-记住,它是无向的-表示每个依赖项。 创建集合列表。 图中有节点吗?如果否,则完成,结果是集合列表。 从图中选择任意节点。 在该节点上运行图遍历并累积可到达的节点集。 把那一套放在一张单子上。 从图形中删除该集中的所有节点。 返回到检查图中的节点。 完成后,您将有一个集合列表,其中每个集合包含相互之间具有某种直接或间接依赖性的任务,并且每个任务都显示在一个集合中。这是由对称依赖等价关系引入的等价类集 更新:关于如何实现这一点,还有一些其他问题 下面是一个简单但不是特别有效的实现。这里的想法是从简单的数据结构构建越来越复杂的数据结构 我想要的第一件事是一本多功能词典。普通字典从键映射到值。我想要一个从键到一组值的映射。我们可以通过NuGet下载任意数量的实现,但编写我们自己的基本实现又快又简单:
public class MultiDictionary<K, V>
{
private readonly Dictionary<K, HashSet<V>> d = new Dictionary<K, HashSet<V>>();
public void Add(K k, V v)
{
if (!d.ContainsKey(k)) d.Add(k, new HashSet<V>());
d[k].Add(v);
}
public void Remove(K k, V v)
{
if (d.ContainsKey(k))
{
d[k].Remove(v);
if (d[k].Count == 0) d.Remove(k);
}
}
public void Remove(K k) => d.Remove(k);
public IEnumerable<V> GetValues(K k) => d.ContainsKey(k) ? d[k] : Enumerable.Empty<V>();
public IEnumerable<K> GetKeys() => d.Keys;
}
太好了。为了简化转换,让我们向有向图中添加一个helper方法:
public UndirectedGraph<T> ToUndirected()
{
var u = new UndirectedGraph<T>();
foreach (T n1 in nodes)
{
u.AddNode(n1);
foreach (T n2 in Edges(n1))
u.AddEdge(n1, n2);
}
return u;
}
public HashSet<T> ReachableNodes(T n) => g.ReachableNodes(n);
有道理吗
更新:删除节点是昂贵的部分,我刚刚意识到,我们不需要这样做。该算法的非破坏性版本为:
static IEnumerable<HashSet<T>> GetEquivalenceClasses<T>(DirectedGraph<T> d)
{
var u = d.ToUndirected();
var results = new List<HashSet<T>>();
var done = new HashSet<T>();
foreach(T current in u.Nodes())
{
if (done.Contains(current))
continue;
HashSet<T> reachable = u.ReachableNodes(current);
results.Add(reachable);
foreach(T n in reachable)
done.Add(n);
}
return results;
}
pniederh的答案给出了联合查找算法的一个过于复杂的版本;正如我在一篇评论中所指出的,关于如何使这些算法有效,有很多研究 在您的特定情况下,另一个有效的算法是: 创建表示无向图的类型。 向表示每个任务的图中添加一个节点。 向图中添加一条边-记住,它是无向的-表示每个依赖项。 创建集合列表。 图中有节点吗?如果否,则完成,结果是集合列表。 从图中选择任意节点。 在该节点上运行图遍历并累积可到达的节点集。 把那一套放在一张单子上。 从图形中删除该集中的所有节点。 返回到检查t中的节点 他画了一幅图。 完成后,您将有一个集合列表,其中每个集合包含相互之间具有某种直接或间接依赖性的任务,并且每个任务都显示在一个集合中。这是由对称依赖等价关系引入的等价类集 更新:关于如何实现这一点,还有一些其他问题 下面是一个简单但不是特别有效的实现。这里的想法是从简单的数据结构构建越来越复杂的数据结构 我想要的第一件事是一本多功能词典。普通字典从键映射到值。我想要一个从键到一组值的映射。我们可以通过NuGet下载任意数量的实现,但编写我们自己的基本实现又快又简单:
public class MultiDictionary<K, V>
{
private readonly Dictionary<K, HashSet<V>> d = new Dictionary<K, HashSet<V>>();
public void Add(K k, V v)
{
if (!d.ContainsKey(k)) d.Add(k, new HashSet<V>());
d[k].Add(v);
}
public void Remove(K k, V v)
{
if (d.ContainsKey(k))
{
d[k].Remove(v);
if (d[k].Count == 0) d.Remove(k);
}
}
public void Remove(K k) => d.Remove(k);
public IEnumerable<V> GetValues(K k) => d.ContainsKey(k) ? d[k] : Enumerable.Empty<V>();
public IEnumerable<K> GetKeys() => d.Keys;
}
太好了。为了简化转换,让我们向有向图中添加一个helper方法:
public UndirectedGraph<T> ToUndirected()
{
var u = new UndirectedGraph<T>();
foreach (T n1 in nodes)
{
u.AddNode(n1);
foreach (T n2 in Edges(n1))
u.AddEdge(n1, n2);
}
return u;
}
public HashSet<T> ReachableNodes(T n) => g.ReachableNodes(n);
有道理吗
更新:删除节点是昂贵的部分,我刚刚意识到,我们不需要这样做。该算法的非破坏性版本为:
static IEnumerable<HashSet<T>> GetEquivalenceClasses<T>(DirectedGraph<T> d)
{
var u = d.ToUndirected();
var results = new List<HashSet<T>>();
var done = new HashSet<T>();
foreach(T current in u.Nodes())
{
if (done.Contains(current))
continue;
HashSet<T> reachable = u.ReachableNodes(current);
results.Add(reachable);
foreach(T n in reachable)
done.Add(n);
}
return results;
}
仅供参考,您描述的算法是联合查找算法的一个示例,因为它需要两个操作:查找与给定项关联的bucket,以及在发现两个bucket需要是同一个bucket时将它们联合在一起。如果你进行文献检索,你会发现许多关于这个主题的论文。这里概述了一些技术:pniederh和Eric,感谢您的回复。非常感谢。仅供参考您所描述的算法是联合查找算法的一个示例,因为它需要两个操作:查找与给定项关联的bucket,以及在发现两个bucket需要是同一个bucket时将它们联合在一起。如果你进行文献检索,你会发现许多关于这个主题的论文。这里概述了一些技术:pniederh和Eric,感谢您的回复。非常感谢,谢谢你的回复。我的图论知识非常有限。因此,我对你的回答有几个问题。我被你在子弹3中的意思弄糊涂了。尤其是“记住它是无向位”。子弹7,我迷路了。你能提供一个C代码段吗?我将编辑我原来的帖子,并添加一些C代码供你使用,如果这会有所帮助。非常感谢。@Cleve:我在项目符号3中的要点是要记住,依赖关系的有向图并不是您所需要的全部信息;您需要知道两个方向的依赖关系。第7点中的算法只是计算关系传递闭包的标准算法;我在这里讨论了一下:哇。非常感谢你,埃里克!这太神奇了。我仍在努力消化一切。在回答您关于使用ToList和Select的问题时,我是否正确地说您需要在更改集合时具体化可枚举项?@Cleve:对于ToList,是的,因为循环正在枚举集合,而循环体正在修改它,所以我想确保我们正在枚举副本。在Select的例子中,这是为了解决这样一个问题:如果某个混蛋调用程序将Ienumerable转换为哈希集,现在我给了某个愚蠢的人或黑客一个对我的一个私有数据结构的引用,他们可以随意处理。Eric,我非常感谢你的帮助。我的最终目标是按照尊重所有依赖关系的顺序执行由图节点表示的测试。我相信,为了让测试尊重依赖关系,需要做我认为是“拓扑排序”的事情?我需要对GetEquivalanceClasses方法生成的每个集合应用这种排序。所以,在我的原始示例中,排序后我将有两个有序集:1{A,B,C,D}未排序->{A,B,D,C}已排序和2{E,F}未排序->{E,F}已排序。下面继续…谢谢你的回复Eric。我的图论知识非常有限。因此,我对你的回答有几个问题。我被你在子弹3中的意思弄糊涂了。尤其是“记住它是无向位”。子弹7,我迷路了。你能提供一个C代码段吗?我将编辑我原来的帖子,并添加一些C代码供你使用,如果这会有所帮助。非常感谢。@Cleve:我在项目符号3中的要点是要记住,依赖关系的有向图并不是您所需要的全部信息;您需要知道两个方向的依赖关系。第7点中的算法只是计算关系传递闭包的标准算法;我在这里讨论了一下:哇。非常感谢你,埃里克!这太神奇了。我仍在努力消化一切。在回答您关于使用ToList和Select的问题时,我是否正确地说您需要在更改集合时具体化可枚举项?@Cleve:对于ToList,是的,因为循环正在枚举集合,而循环体正在修改
我想确定我们正在枚举一个副本。在Select的例子中,这是为了解决这样一个问题:如果某个混蛋调用程序将Ienumerable转换为哈希集,现在我给了某个愚蠢的人或黑客一个对我的一个私有数据结构的引用,他们可以随意处理。Eric,我非常感谢你的帮助。我的最终目标是按照尊重所有依赖关系的顺序执行由图节点表示的测试。我相信,为了让测试尊重依赖关系,需要做我认为是“拓扑排序”的事情?我需要对GetEquivalanceClasses方法生成的每个集合应用这种排序。所以,在我的原始示例中,排序后我将有两个有序集:1{A,B,C,D}未排序->{A,B,D,C}已排序和2{E,F}未排序->{E,F}已排序。下面继续……你在这里有了一个良好的开端。我在回答中添加了一个实现,它显示了我如何解决这个问题。您在这里有了一个良好的开端。我在回答中添加了一个实现,显示了如何解决这个问题。