// Copyright Epic Games, Inc. All Rights Reserved. #include "GraphConvert.h" #include "Misc/AutomationTest.h" #if WITH_DEV_AUTOMATION_TESTS #include "Algo/Compare.h" #endif namespace Algo::Graph { void ConstructTransposeGraph(TConstArrayView> Graph, TArray64& OutTransposeGraphBuffer, TArray>& OutTransposeGraph, EConvertToGraphOptions Options) { int32 NumVertices = Graph.Num(); TArray> TransposeGraph; TransposeGraph.SetNum(NumVertices); for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { for (FVertex Edge : Graph[Vertex]) { // Normalize Step 1: Remove edges to self if (Edge == Vertex) { continue; } TransposeGraph[Edge].Add(Vertex); } } // Finish normalizing; we have to do this before converting to the buffer format // because we need the duplicates removed to get an accurate count int32 NumTotalEdges = 0; for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { TArray& Edges = TransposeGraph[Vertex]; // Normalize Step 2: Sort edges Algo::Sort(Edges); // Normalize Step 3: Remove duplicates Edges.SetNum(Algo::Unique(Edges), EAllowShrinking::No); NumTotalEdges += Edges.Num(); } // Convert the Array of Arrays into the output VertexBuffer and Array of Arrayviews if (EnumHasAnyFlags(Options, EConvertToGraphOptions::Shrink)) { OutTransposeGraphBuffer.Empty(NumTotalEdges); OutTransposeGraph.Empty(NumVertices); } else { OutTransposeGraphBuffer.Reset(NumTotalEdges); OutTransposeGraph.Reset(NumVertices); } FVertex* BufferData = OutTransposeGraphBuffer.GetData(); for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { TArray& Edges = TransposeGraph[Vertex]; OutTransposeGraph.Emplace(BufferData + OutTransposeGraphBuffer.Num(), Edges.Num()); OutTransposeGraphBuffer.Append(Edges); } check(OutTransposeGraphBuffer.Num() == NumTotalEdges); check(BufferData == OutTransposeGraphBuffer.GetData()); // We have TArrayViews into it } bool TryConstructCondensationGraph(TConstArrayView> Graph, TArray64& OutGraphBuffer, TArray>& OutGraph, TArray64* OutOutVertexToInVerticesBuffer, TArray>* OutOutVertexToInVertices, TArray* OutInVertexToOutVertex, EConvertToGraphOptions Options) { // Kosaraju-Sharir's algorithm - order the vertices from first out in a DFS search to last out. Any vertex that is // referenced by a referencer vertex that exited the dfs earlier is in a strongly connected component with the // referencer vertex. // Provide local buffers for any optional outputs that were not provided checkf((OutOutVertexToInVerticesBuffer != nullptr) == (OutOutVertexToInVertices != nullptr), TEXT("OutOutVertexToInVerticesBuffer and OutOutVertexToInVertices must either both be null or both be non-null.")); TArray64 LocalBufferOutVertexToInVerticesBuffer; TArray> LocalBufferOutVertexToInVertices; TArray LocalBufferInVertexToOutVertex; TArray64& OutVertsBuffer(OutOutVertexToInVerticesBuffer ? *OutOutVertexToInVerticesBuffer : LocalBufferOutVertexToInVerticesBuffer); TArray>& OutVertexToInVertices(OutOutVertexToInVertices ? *OutOutVertexToInVertices : LocalBufferOutVertexToInVertices); TArray& InVertexToOutVertex(OutInVertexToOutVertex ? *OutInVertexToOutVertex : LocalBufferInVertexToOutVertex); int32 NumVertices = Graph.Num(); enum class EVisitStatus : uint8 { NotVisited, InProgress, Visited, }; struct FVisitData { FVertex Vertex; int32 NextEdge; }; TArray DFSOutFirstToLast; bool bHasCycle = false; TArray Stack; Stack.Reserve(NumVertices); int32 NumEdges = 0; // Create the DFSOutFirstToLast order. DepthFirstSearch the input graph and record each vertex after its edges. // Record whether we find any cycles during the search. { TArray VisitStatus; Stack.Reset(); VisitStatus.SetNumUninitialized(NumVertices); DFSOutFirstToLast.Reserve(NumVertices); for (FVertex Vertex = 0; Vertex < NumVertices; ++Vertex) { VisitStatus[Vertex] = EVisitStatus::NotVisited; } for (FVertex Root = 0; Root < NumVertices; ++Root) { if (VisitStatus[Root] != EVisitStatus::NotVisited) { continue; } VisitStatus[Root] = EVisitStatus::InProgress; Stack.Add({ Root, 0 }); while (!Stack.IsEmpty()) { FVisitData& VisitData = Stack.Last(); FVertex Vertex = VisitData.Vertex; TConstArrayView Edges = Graph[Vertex]; bool bPushed = false; while (VisitData.NextEdge < Edges.Num()) { FVertex EdgeVertex = Edges[VisitData.NextEdge++]; EVisitStatus& EdgeVisitStatus = VisitStatus[EdgeVertex]; if (EdgeVisitStatus == EVisitStatus::Visited) { } else if (EdgeVisitStatus == EVisitStatus::NotVisited) { EdgeVisitStatus = EVisitStatus::InProgress; Stack.Add({ EdgeVertex, 0 }); bPushed = true; break; } else // EVisitStatus::InProgress { // Don't count edges to self as a cycle, they are removed in normalization of the input graph if (EdgeVertex != Vertex) { bHasCycle = true; } } } if (!bPushed) { NumEdges += Edges.Num(); DFSOutFirstToLast.Add(Vertex); VisitStatus[Vertex] = EVisitStatus::Visited; Stack.Pop(); } } } } // Allocate the OutVertexToInVertices output values, using NumVertices as a conservative estimate of NumOutVertices bool bShrink = EnumHasAnyFlags(Options, EConvertToGraphOptions::Shrink); if (bHasCycle || OutOutVertexToInVertices || OutInVertexToOutVertex) { if (bShrink) { OutVertexToInVertices.Empty(NumVertices); OutVertsBuffer.Empty(NumVertices); } else { OutVertexToInVertices.Reset(NumVertices); OutVertsBuffer.Reset(NumVertices); } } // If we did not find any cycles, then the CondensationGraph is equal to the input Graph, so return false. if (!bHasCycle) { OutGraphBuffer.Reset(); OutGraph.Reset(); if (OutOutVertexToInVertices || OutInVertexToOutVertex) { if (bShrink) { InVertexToOutVertex.Empty(NumVertices); } else { InVertexToOutVertex.Reset(NumVertices); } // Copy reversed DFSOutFirstToLast into OutVertexToInVertices, and inverted into InVertexToOutVertex OutVertexToInVertices.SetNum(NumVertices, EAllowShrinking::No); OutVertsBuffer.SetNum(NumVertices, EAllowShrinking::No); InVertexToOutVertex.SetNum(NumVertices, EAllowShrinking::No); for (FVertex OutVertex = 0; OutVertex < NumVertices; ++OutVertex) { FVertex InVertex = DFSOutFirstToLast[NumVertices - 1 - OutVertex]; OutVertsBuffer[OutVertex] = InVertex; OutVertexToInVertices[OutVertex] = TConstArrayView(OutVertsBuffer.GetData() + OutVertex, 1); InVertexToOutVertex[InVertex] = OutVertex; } } return false; } // Get the TransposeGraph TArray64 ReferenceGraphBuffer; TArray> ReferenceGraph; ConstructTransposeGraph(Graph, ReferenceGraphBuffer, ReferenceGraph); TBitArray<> Assigned; // Iterate vertices from last DFSOut to first DFSOut. Each one that is not already assigned creates its own // strongly connected component. Any that are not involved in a cycle will have a strongly connected component // that consists of a single vertex. When we find a vertex that is not already assigned, visit all unassigned // vertices in its reference graph and assign them all to its component. { Stack.Reset(); Assigned.Init(false, NumVertices); FVertex* OutVertsData = OutVertsBuffer.GetData(); for (int32 IndexOfRoot = NumVertices - 1; IndexOfRoot >= 0; --IndexOfRoot) { FVertex Root = DFSOutFirstToLast[IndexOfRoot]; if (Assigned[Root]) { continue; } Assigned[Root] = true; int64 InitialOffset = OutVertsBuffer.Num(); OutVertsBuffer.Add(Root); Stack.Add({ Root, 0 }); while (!Stack.IsEmpty()) { FVisitData& VisitData = Stack.Last(); TConstArrayView Edges = ReferenceGraph[VisitData.Vertex]; bool bPushed = false; while (VisitData.NextEdge < Edges.Num()) { FVertex EdgeVertex = Edges[VisitData.NextEdge++]; if (Assigned[EdgeVertex]) { } else { Assigned[EdgeVertex] = true; OutVertsBuffer.Add(EdgeVertex); Stack.Add({ EdgeVertex, 0 }); bPushed = true; break; } } if (!bPushed) { Stack.Pop(); } } OutVertexToInVertices.Emplace(OutVertsData + InitialOffset, OutVertsBuffer.Num() - InitialOffset); } // We have TArrayViews into OutVertsBuffer; assert its data buffer did not change check(OutVertsBuffer.GetData() == OutVertsData); } int32 NumOutVertices = OutVertexToInVertices.Num(); // Allocate the output variables that depend on NumOutVertices if (bShrink) { // Shrink OutVertexToInVertices and OutVertexToInVerticesBuffer if requested if (OutOutVertexToInVertices) { TArray64 CopyBuffer = OutVertsBuffer; FVertex* OutVertsData = OutVertsBuffer.GetData(); FVertex* CopyData = CopyBuffer.GetData(); OutVertexToInVertices.Shrink(); for (TConstArrayView& InVertices : OutVertexToInVertices) { InVertices = TConstArrayView(CopyData + (InVertices.GetData() - OutVertsData), InVertices.Num()); } OutVertsBuffer = MoveTemp(CopyBuffer); } InVertexToOutVertex.Empty(NumVertices); OutGraph.Empty(NumOutVertices); } else { InVertexToOutVertex.Reset(NumVertices); OutGraph.Reset(NumOutVertices); } // We don't know NumEdges in the CondensationGraph yet; NumEdges from the original graph is a conservative estimate OutGraphBuffer.Reset(NumEdges); // Invert OutVertexToInVertices to create InVertexToOutVertex InVertexToOutVertex.SetNumUninitialized(NumVertices); for (FVertex OutVertex = 0; OutVertex < NumOutVertices; ++OutVertex) { for (FVertex InVertex : OutVertexToInVertices[OutVertex]) { InVertexToOutVertex[InVertex] = OutVertex; } } // Create the edges of the condensationgraph by converting each source vertex edge vertex into the condensation // edge vertex and unioning all transformed edge vertices of all source vertices for the condensation root vertex { Assigned.SetRange(0, NumVertices, false); FVertex* OutGraphData = OutGraphBuffer.GetData(); for (FVertex OutVertex = 0; OutVertex < NumOutVertices; ++OutVertex) { // Uniquely add all of the unioned edge vertices for this OutVertex onto a range in OutGraphBuffer int64 InitialOffset = OutGraphBuffer.Num(); for (FVertex InVertex : OutVertexToInVertices[OutVertex]) { for (FVertex SourceEdgeVertex : Graph[InVertex]) { FVertex OutEdgeVertex = InVertexToOutVertex[SourceEdgeVertex]; if (!Assigned[OutEdgeVertex]) // Note this condition also removes edges to self { Assigned[OutEdgeVertex] = true; OutGraphBuffer.Add(OutEdgeVertex); } } } // Record that range of edges in our OutGraph TConstArrayView& OutEdges = OutGraph.Emplace_GetRef(OutGraphData + InitialOffset, OutGraphBuffer.Num() - InitialOffset); // Clear the values in Assigned that we set; this is faster than clearing the whole array for (FVertex OutEdgeVertex : OutEdges) { Assigned[OutEdgeVertex] = false; } } // We have TArrayViews into OutVertexToInVertexBuffer; assert its data buffer did not change check(OutGraphBuffer.GetData() == OutGraphData); // Shrink OutGraphBuffer if requested if (bShrink) { TArray64 CopyBuffer = OutGraphBuffer; FVertex* CopyData = CopyBuffer.GetData(); for (TConstArrayView& Edges : OutGraph) { Edges = TConstArrayView(CopyData + (Edges.GetData() - OutGraphData), Edges.Num()); } OutGraphBuffer = MoveTemp(CopyBuffer); check(OutGraphBuffer.GetData() == CopyData); } } return true; } void ConstructPartialTransposeGraph(TConstArrayView> Graph, TArrayView InVertices, int64 MaxOutGraphEdges, TArray64& OutTransposeGraphBuffer, TArray>& OutTransposeGraph, TArray& OutInVerticesInOutGraph) { OutInVerticesInOutGraph.Reset(InVertices.Num()); Algo::Sort(InVertices, [Graph](FVertex A, FVertex B)\ { int32 NumA = Graph[A].Num(); int32 NumB = Graph[B].Num(); return NumA > NumB || (NumA == NumB && A < B); }); int32 NumVertices = Graph.Num(); TArray> TransposeGraph; TransposeGraph.SetNum(NumVertices); int32 NumEdges = 0; for (FVertex InVertex : InVertices) { NumEdges += Graph[InVertex].Num(); if (NumEdges > MaxOutGraphEdges) { NumEdges -= Graph[InVertex].Num(); break; } OutInVerticesInOutGraph.Add(InVertex); for (FVertex TargetVertex : Graph[InVertex]) { TransposeGraph[TargetVertex].Add(InVertex); } } ConvertToSingleBufferGraph(TransposeGraph, OutTransposeGraphBuffer, OutTransposeGraph); } } #if WITH_DEV_AUTOMATION_TESTS IMPLEMENT_SIMPLE_AUTOMATION_TEST(FCondensationGraphTest, "System.Core.Algo.CondensationGraph", EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter); bool FCondensationGraphTest::RunTest(const FString& Parameters) { using namespace Algo::Graph; TArray> Buffer; TArray> Graph; TArray64 CondensationBuffer; TArray> CondensationGraph; TArray64 OutVertexToInVerticesBuffer; TArray> OutVertexToInVertices; TArray InVertexToOutVertex; TArray64> ExpectedOutVertexToInVertices; TArray ExpectedInVertexToOutVertex; TArray ActualInVertexToOutVertex; TArray ExpectedComponentVerticesScratch; TArray ActualComponentVerticesScratch; TArray ExpectedOutEdges; TArray ActualOutEdges; TArray Reachable; TBitArray<> Assigned; auto AddVertex = [&Buffer, &Graph](FVertex Vertex, TConstArrayView Edges) { if (Graph.Num() <= Vertex) { Graph.SetNum(Vertex + 1); Buffer.SetNum(Vertex + 1); } Buffer[Vertex] = Edges; Graph[Vertex] = Buffer[Vertex]; }; auto AddExpectedComponent = [&ExpectedOutVertexToInVertices](TConstArrayView InputVertices) { ExpectedOutVertexToInVertices.Emplace_GetRef().Append(InputVertices); }; auto Clear = [&Buffer, &Graph, &ExpectedOutVertexToInVertices]() { Buffer.Reset(); Graph.Reset(); ExpectedOutVertexToInVertices.Reset(); }; auto WriteArrayToString = [](TConstArrayView A) { TStringBuilder<256> Writer; Writer << TEXT("["); Writer.Join(A, TEXT(',')); Writer << TEXT("]"); return FString(Writer); }; auto ConfirmResults = [this, &Graph, &CondensationBuffer, &CondensationGraph, &OutVertexToInVerticesBuffer, &OutVertexToInVertices, &InVertexToOutVertex , &ExpectedOutVertexToInVertices, &ExpectedInVertexToOutVertex, &Assigned, &ActualInVertexToOutVertex, &ExpectedComponentVerticesScratch, &ActualComponentVerticesScratch, &WriteArrayToString, &ExpectedOutEdges, &ActualOutEdges, &Reachable] (const TCHAR* TestCaseName) { bool bResult = TryConstructCondensationGraph(Graph, CondensationBuffer, CondensationGraph, &OutVertexToInVerticesBuffer, &OutVertexToInVertices, &InVertexToOutVertex, EConvertToGraphOptions::Shrink); bool bExpectedResult = !ExpectedOutVertexToInVertices.IsEmpty(); if (bResult != bExpectedResult) { AddError(FString::Printf(TEXT("Failed for case \"%s\": expected ConstructCondensationGraph to return %s but it returned %s."), TestCaseName, bExpectedResult ? TEXT("true") : TEXT("false"), bResult ? TEXT("true") : TEXT("false"))); return; } int32 NumVertices = Graph.Num(); int32 NumOutVertices = CondensationGraph.Num(); Assigned.SetNumUninitialized(NumVertices); // Verify InVertexToOutVertex and OutVertexToInVertices match, cover all vertices, and do not put the same // invertex into two outvertices ActualInVertexToOutVertex.SetNumUninitialized(NumVertices, EAllowShrinking::No); Assigned.SetRange(0, NumVertices, false); int32 ComponentIndex = 0; for (TConstArrayView ActualComponentVertices : OutVertexToInVertices) { for (FVertex InVertex : ActualComponentVertices) { if (Assigned[InVertex]) { AddError(FString::Printf(TEXT("Failed for case \"%s\": OutVertexToInVertices has the same invertex %d in both outvertex %d and outvertex %d."), TestCaseName, (int32)InVertex, (int32)ActualInVertexToOutVertex[InVertex], (int32)ComponentIndex)); return; } Assigned[InVertex] = true; ActualInVertexToOutVertex[InVertex] = ComponentIndex; } ++ComponentIndex; } for (FVertex InVertex = 0; InVertex < NumVertices; ++InVertex) { if (!Assigned[InVertex]) { AddError(FString::Printf(TEXT("Failed for case \"%s\": OutVertexToInVertices is missing invertex %d; it did not assign that vertex to any outvertex."), TestCaseName, (int32) InVertex)); return; } if (ActualInVertexToOutVertex[InVertex] != InVertexToOutVertex[InVertex]) { AddError(FString::Printf(TEXT("Failed for case \"%s\": OutVertexToInVertices has outvertex %d <- invertex %d, but InVertexToOutVertex has invertex %d -> outvertex %d."), TestCaseName, (int32)ActualInVertexToOutVertex[InVertex], (int32)InVertex, (int32)InVertex, (int32)InVertexToOutVertex[InVertex])); } } // Finish the construction of ExpectedOutVertexToInVertices; it has the contract that any unmentioned vertex is in a // component by itself. Also construct its inverse - ExpectedInVertexToOutVertex. ExpectedInVertexToOutVertex.SetNumUninitialized(NumVertices, EAllowShrinking::No); Assigned.SetRange(0, NumVertices, false); ComponentIndex = 0; for (TArray& ExpectedComponentVertices : ExpectedOutVertexToInVertices) { for (FVertex InVertex : ExpectedComponentVertices) { Assigned[InVertex] = true; ExpectedInVertexToOutVertex[InVertex] = ComponentIndex; } ++ComponentIndex; } for (FVertex InVertex = 0; InVertex < NumVertices; ++InVertex) { if (!Assigned[InVertex]) { ExpectedInVertexToOutVertex[InVertex] = ComponentIndex++; ExpectedOutVertexToInVertices.Add({ InVertex }); } } // Verify that the actual components place every InVertex in a component with the same list of Inputvertices // as the expected components Assigned.SetRange(0, NumVertices, false); for (FVertex InVertex = 0; InVertex < NumVertices; ++InVertex) { if (Assigned[InVertex]) { continue; } FVertex ExpectedOutVertex = ExpectedInVertexToOutVertex[InVertex]; FVertex ActualOutVertex = ActualInVertexToOutVertex[InVertex]; ExpectedComponentVerticesScratch.Reset(); ExpectedComponentVerticesScratch.Append(ExpectedOutVertexToInVertices[ExpectedOutVertex]); ActualComponentVerticesScratch.Reset(); ActualComponentVerticesScratch.Append(OutVertexToInVertices[ActualOutVertex]); Algo::Sort(ExpectedComponentVerticesScratch); Algo::Sort(ActualComponentVerticesScratch); if (!Algo::Compare(ExpectedComponentVerticesScratch, ActualComponentVerticesScratch)) { AddError(FString::Printf(TEXT("Failed for case \"%s\": Input vertex %d was placed into a component with invertices %s, but it was expected in a component with invertices %s."), TestCaseName, (int32)InVertex, *WriteArrayToString(ActualComponentVerticesScratch), *WriteArrayToString(ExpectedComponentVerticesScratch))); return; } } // Verify that every edge in the input graph is a component edge in the output graph, or is inside the component if (bResult) { for (FVertex InSourceVertex = 0; InSourceVertex < NumVertices; ++InSourceVertex) { FVertex OutSourceVertex = ActualInVertexToOutVertex[InSourceVertex]; for (FVertex InEdgeVertex : Graph[InSourceVertex]) { FVertex ExpectedOutEdgeVertex = ActualInVertexToOutVertex[InEdgeVertex]; if (ExpectedOutEdgeVertex == OutSourceVertex) { continue; } if (!CondensationGraph[OutSourceVertex].Contains(ExpectedOutEdgeVertex)) { AddError(FString::Printf(TEXT("Failed for case \"%s\": Missing edge in the condensation graph: ") TEXT("expected edge from vertex %d in component %d to vertex %d in component %d, but this edge is missing."), TestCaseName, (int32)InSourceVertex, (int32)OutSourceVertex, (int32)InEdgeVertex, (int32)ExpectedOutEdgeVertex)); return; } } } // Verify that no component edge exists in the output graph unless there is a witness edge for it in the input graph // Create the expected edges of the condensationgraph by converting each source vertex edge vertex into the condensation // edge vertex and unioning all transformed edge vertices of all source vertices for the condensation root vertex for (FVertex OutVertex = 0; OutVertex < NumOutVertices; ++OutVertex) { Assigned.SetRange(0, NumOutVertices, false); ExpectedOutEdges.Reset(); for (FVertex InVertex : OutVertexToInVertices[OutVertex]) { for (FVertex SourceEdgeVertex : Graph[InVertex]) { FVertex OutEdgeVertex = ActualInVertexToOutVertex[SourceEdgeVertex]; if (!Assigned[OutEdgeVertex]) { Assigned[OutEdgeVertex] = true; ExpectedOutEdges.Add(OutEdgeVertex); } } } ActualOutEdges.Reset(); ActualOutEdges.Append(CondensationGraph[OutVertex]); Algo::Sort(ExpectedOutEdges); Algo::Sort(ActualOutEdges); if (!Algo::Compare(ActualOutEdges, ExpectedOutEdges)) { AddError(FString::Printf(TEXT("Failed for case \"%s\": Expected component %d to have edges %s, but it had %s."), TestCaseName, (int32)OutVertex, *WriteArrayToString(ActualOutEdges), *WriteArrayToString(ExpectedOutEdges))); return; } } // Verify the output is sorted from root to leaf for (FVertex Root = 0; Root < NumOutVertices; ++Root) { Assigned.SetRange(0, NumOutVertices, false); Reachable.Reset(NumOutVertices); Assigned[Root] = true; Reachable.Add(Root); int32 NextIndex = 0; while (NextIndex < Reachable.Num()) { FVertex Vertex = Reachable.Pop(EAllowShrinking::No); for (FVertex EdgeVertex : CondensationGraph[Vertex]) { if (!Assigned[EdgeVertex]) { Assigned[EdgeVertex] = true; Reachable.Add(EdgeVertex); } } } for (FVertex ReachableVertex : Reachable) //-V1078 { if (ReachableVertex < Root) { AddError(FString::Printf(TEXT("Failed for case \"%s\": Expected outgraph to be topologically sorted from root to leaf, but vertex %d is reachable from vertex %d."), TestCaseName, (int32)ReachableVertex, (int32)Root)); return; } } } } else // !if (bResult) { // Should have cleared the graph arguments if (!CondensationGraph.IsEmpty() || !CondensationBuffer.IsEmpty()) { AddError(FString::Printf(TEXT("Failed for case \"%s\": Expected outgraph to be cleared since result was false, but it is non-empty."), TestCaseName)); return; } } }; { Clear(); AddVertex(0, { }); AddVertex(1, { 0 }); AddVertex(2, { 1 }); ConfirmResults(TEXT("Each node depends on the previous one")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 2 }); AddVertex(2, { }); ConfirmResults(TEXT("Each node depends on the next one")); } { Clear(); AddVertex(0, { 0 }); AddVertex(1, { 0,1 }); AddVertex(2, { 1,2 }); ConfirmResults(TEXT("SelfReferences")); } { // 6 // / \ // 5 7 // / \ \ // 0 1 8 // \ / \ \ // 3 4 | // \ \ / // \ 9 // \ / // 2 Clear(); AddVertex(6, { 5,7 }); AddVertex(5, { 0,1 }); AddVertex(7, { 8 }); AddVertex(0, { 3 }); AddVertex(1, { 3,4 }); AddVertex(8, { 9 }); AddVertex(3, { 2 }); AddVertex(4, { 9 }); AddVertex(9, { 2 }); AddVertex(2, { }); ConfirmResults(TEXT("SketchedOutExample1")); } { Clear(); AddVertex(0, { 0,1 }); AddVertex(1, { 0,1 }); AddExpectedComponent({ 0, 1 }); ConfirmResults(TEXT("Simple cycle")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 0,1,2 }); AddVertex(2, { 2,0 }); AddExpectedComponent({ 0, 1, 2 }); ConfirmResults(TEXT("Short cycle in a long cycle")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 0,2 }); AddVertex(2, { 3 }); AddVertex(3, { }); AddExpectedComponent({ 0, 1 }); ConfirmResults(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts")); } { Clear(); AddVertex(0, { }); AddVertex(1, { 0 }); AddVertex(2, { 1,3 }); AddVertex(3, { 2 }); AddExpectedComponent({ 2, 3 }); ConfirmResults(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts, submitted in reverse")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 2 }); AddVertex(2, { 3 }); AddVertex(3, { 2 }); AddExpectedComponent({ 2,3 }); ConfirmResults(TEXT("Cycle at a leaf and a chain from the root depending on that cycle")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 0 }); AddVertex(2, { 1 }); AddVertex(3, { 2 }); AddExpectedComponent({ 0, 1 }); ConfirmResults(TEXT("Cycle in the root and with the root cycle depending on a chain of non-cycle verts")); } { Clear(); AddVertex(0, { 1 }); AddVertex(1, { 2,3 }); AddVertex(2, { 1,3 }); AddVertex(3, { 1,2 }); AddExpectedComponent({ 1,2,3 }); ConfirmResults(TEXT("Vertex dependent upon a cycle")); } { // 0 -> 1 -> 2 -> 3 -> 1 // | // v // 4 -> 5 -> 6 -> 4 Clear(); AddVertex(0, { 1 }); AddVertex(1, { 2 }); AddVertex(2, { 3,5 }); AddVertex(3, { 1 }); AddVertex(4, { 5 }); AddVertex(5, { 6 }); AddVertex(6, { 4 }); AddExpectedComponent({ 1,2,3 }); AddExpectedComponent({ 4,5,6 }); ConfirmResults(TEXT("One cycle dependent upon another")); } { // 5 -> 0 -> (1 -> 2 -> 1) // | | // | v // | 5 // v // (3 -> 4 -> 3) Clear(); AddVertex(0, { 1,3 }); AddVertex(1, { 2 }); AddVertex(2, { 1,5 }); AddVertex(3, { 4 }); AddVertex(4, { 3 }); AddVertex(5, { 0 }); AddExpectedComponent({ 0,1,2,5 }); AddExpectedComponent({ 3,4 }); ConfirmResults(TEXT("MutuallyReachableSet Problem1")); } { Clear(); TArray AllButItself; TArray All; for (FVertex Vertex = 0; Vertex < 6; ++Vertex) { AllButItself.Add(Vertex); All.Add(Vertex); } for (FVertex Vertex = 0; Vertex < 6; ++Vertex) { AllButItself.Remove(Vertex); AddVertex(Vertex, AllButItself); AllButItself.Add(Vertex); } AddExpectedComponent(All); ConfirmResults(TEXT("FullyConnectedGraph")); } { Clear(); // 0 - 1 - 2 - 0 // | // 3 - 4 - 5 - 3 // | // 6 - 7 - 8 - 6 // | // 1 AddVertex(0, { 1,3 }); AddVertex(1, { 2 }); AddVertex(2, { 0 }); AddVertex(3, { 4 }); AddVertex(4, { 5 }); AddVertex(5, { 3,8 }); AddVertex(6, { 7 }); AddVertex(7, { 1,8 }); AddVertex(8, { 6 }); AddExpectedComponent({ 0,1,2,3,4,5,6,7,8 }); ConfirmResults(TEXT("Cycle of cycles")); } { Clear(); // 0 ----- 1 ----- 2 ----- 0 // | | | // | 0 | // | | // 5 - 4 - 3 - 5 6 - 7 - 6 // |\ | // | 5 | // | | // 8 - 9 - 10 - 9 11 // | | | // 10 8 8 AddVertex(0, { 1,5 }); AddVertex(1, { 0,2 }); AddVertex(2, { 0,6 }); AddVertex(3, { 5 }); AddVertex(4, { 3,5,8 }); AddVertex(5, { 4 }); AddVertex(6, { 7 }); AddVertex(7, { 6,11 }); AddVertex(8, { 9,10 }); AddVertex(9, { 8,10 }); AddVertex(10, { 8,9 }); AddVertex(11, { }); AddExpectedComponent({ 0,1,2 }); AddExpectedComponent({ 3,4,5 }); AddExpectedComponent({ 6,7 }); AddExpectedComponent({ 8,9,10 }); ConfirmResults(TEXT("Tree of cycles")); } return true; } #endif