diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index a6a59da..01e21d9 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,9 +1,9 @@ name: Build -on: [push, pull_request] +on: [push] jobs: - build: + build-Windows: runs-on: windows-latest @@ -20,4 +20,19 @@ jobs: - name: Test run: $slnList = Get-ChildItem $foo.FullName -Recurse -Filter '*.sln'; foreach ($file in $slnList) {dotnet test $file.FullName} + build-Ubuntu: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Build + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.x' + - name: Restore + run: for f in $(find . -name "*.sln"); do dotnet restore $f; done + - name: Build + run: for f in $(find . -name "*.sln"); do dotnet build $f; done + - name: Test + run: for f in $(find . -name "*.sln"); do dotnet test $f; done diff --git a/Routers/Routers/GraphTest/GraphTest.cs b/Routers/Routers/GraphTest/GraphTest.cs new file mode 100644 index 0000000..83c48b4 --- /dev/null +++ b/Routers/Routers/GraphTest/GraphTest.cs @@ -0,0 +1,114 @@ +namespace GraphTest; + +using NUnit.Framework; +using Routers; + +public class Tests +{ + IGraph graph = new Graph(); + + [SetUp] + public void Setup() + { + graph = new Graph(); + } + + [Test] + public void ShouldExpectedFalseWhenIsTheGraphConnectedForDisconnectGraph() + { + graph.AddNode(2); + graph.AddNode(4); + graph.AddNode(6); + graph.AddNode(7); + graph.AddNode(1); + graph.AddNode(5); + graph.AddNode(9); + graph.AddNode(8); + + graph.SetLength(6, 9, 12); + graph.SetLength(2, 4, 3); + graph.SetLength(2, 6, 4); + graph.SetLength(2, 7, 8); + graph.SetLength(2, 5, 2); + graph.SetLength(7, 8, 13); + + Assert.False(graph.IsTheGraphConnected()); + } + + [Test] + public void ShouldExpectedTrueWhenIsTheGraphConnectedForGraphConsisting1Node() + { + graph.AddNode(2); + Assert.True(graph.IsTheGraphConnected()); + } + + [Test] + public void ShouldExpectedTrueWhenIsTheGraphConnectedForEmptyGraph() + { + Assert.True(graph.IsTheGraphConnected()); + } + + [Test] + public void ShouldThrowsDisconnectedGraphWhenBuildAcyclicGraphForDisconnectGraph() + { + graph.AddNode(2); + graph.AddNode(4); + graph.AddNode(6); + graph.AddNode(7); + graph.AddNode(1); + graph.AddNode(5); + graph.AddNode(9); + graph.AddNode(8); + + graph.SetLength(2, 4, 3); + graph.SetLength(2, 6, 4); + graph.SetLength(2, 7, 8); + graph.SetLength(2, 5, 2); + graph.SetLength(7, 8, 13); + + Assert.Throws(() => graph.BuildAcyclicGraph()); + } + + [Test] + public void ShouldExpectedTrueWhenIsTheGraphConnectedForConnectedGraph() + { + graph.AddNode(2); + graph.AddNode(4); + graph.AddNode(6); + graph.AddNode(7); + graph.AddNode(1); + graph.AddNode(5); + graph.AddNode(9); + graph.AddNode(8); + + graph.SetLength(1, 6, 12); + graph.SetLength(6, 9, 12); + graph.SetLength(2, 4, 3); + graph.SetLength(2, 6, 4); + graph.SetLength(2, 7, 8); + graph.SetLength(2, 5, 2); + graph.SetLength(7, 8, 13); + + Assert.True(graph.IsTheGraphConnected()); + } + + [Test] + public void ShouldExpectedFalseWhenAddExistingNode() + { + graph.AddNode(2); + Assert.False(graph.AddNode(2)); + } + + [Test] + public void ShouldExpectedFalseWhenSetLengthForNonExistingNode() + { + Assert.False(graph.SetLength(2, 6, 4)); + } + + [Test] + public void DisconnectedGraph() + { + graph.BuildGraph("..//..//..//InputTest.txt"); + Assert.Throws(() => graph.BuildAcyclicGraph()); + } +} \ No newline at end of file diff --git a/Routers/Routers/GraphTest/GraphTest.csproj b/Routers/Routers/GraphTest/GraphTest.csproj new file mode 100644 index 0000000..97caa87 --- /dev/null +++ b/Routers/Routers/GraphTest/GraphTest.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + + false + + + + + + + + + + + + + + diff --git a/Routers/Routers/GraphTest/InputTest.txt b/Routers/Routers/GraphTest/InputTest.txt new file mode 100644 index 0000000..2170138 --- /dev/null +++ b/Routers/Routers/GraphTest/InputTest.txt @@ -0,0 +1,3 @@ +1 : 4 (3), 6 (5) +2 : 3 (8) +6 : 8 (12) diff --git a/Routers/Routers/Routers.sln b/Routers/Routers/Routers.sln new file mode 100644 index 0000000..07ac4aa --- /dev/null +++ b/Routers/Routers/Routers.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32228.430 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Routers", "Routers\Routers.csproj", "{78B74291-487A-4340-AE29-2114D18ED214}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphTest", "GraphTest\GraphTest.csproj", "{FC895312-F6AB-48CF-A16B-4B7E8CCB7A88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {78B74291-487A-4340-AE29-2114D18ED214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78B74291-487A-4340-AE29-2114D18ED214}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78B74291-487A-4340-AE29-2114D18ED214}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78B74291-487A-4340-AE29-2114D18ED214}.Release|Any CPU.Build.0 = Release|Any CPU + {FC895312-F6AB-48CF-A16B-4B7E8CCB7A88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC895312-F6AB-48CF-A16B-4B7E8CCB7A88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC895312-F6AB-48CF-A16B-4B7E8CCB7A88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC895312-F6AB-48CF-A16B-4B7E8CCB7A88}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {31700D31-9087-4D39-BA64-4DC5A05C25BE} + EndGlobalSection +EndGlobal diff --git a/Routers/Routers/Routers/Comparator.cs b/Routers/Routers/Routers/Comparator.cs new file mode 100644 index 0000000..0e61ceb --- /dev/null +++ b/Routers/Routers/Routers/Comparator.cs @@ -0,0 +1,20 @@ +namespace Routers; + +using System.Collections.Generic; + +public class Comparator : IComparer +{ + int IComparer.Compare(int x, int y) + { + if (x > y) + { + return -1; + } + else if (x == y) + { + return 0; + } + return 1; + } +} + diff --git a/Routers/Routers/Routers/DisconnectedGraphException.cs b/Routers/Routers/Routers/DisconnectedGraphException.cs new file mode 100644 index 0000000..fd7e0c5 --- /dev/null +++ b/Routers/Routers/Routers/DisconnectedGraphException.cs @@ -0,0 +1,9 @@ +namespace Routers; + +/// +/// A class for creating custom exceptions +/// +public class DisconnectedGraph : Exception +{ + public DisconnectedGraph() : base() { } +} diff --git a/Routers/Routers/Routers/IGraph.cs b/Routers/Routers/Routers/IGraph.cs new file mode 100644 index 0000000..885096c --- /dev/null +++ b/Routers/Routers/Routers/IGraph.cs @@ -0,0 +1,44 @@ +namespace Routers; + +public interface IGraph +{ + /// + /// Function for adding a node + /// + /// Node value + /// Was there a value in the graph + public bool AddNode(int index); + + /// + /// Function for creating an edge between vertices + /// + /// Value of the first node + /// Value of the second node + /// Edge length + /// Was there an edge in the graph + public bool SetLength(int valueFirstNode, int valueSecondNode, int length); + + /// + /// Function for constructing a new graph without cycles + /// + /// Acyclic graph, with maximum sum of edges + public Graph BuildAcyclicGraph(); + + /// + /// Function for graph construction + /// + /// The path to the file with the table + public void BuildGraph(string pathToFile); + + /// + /// Function for graph printing + /// + /// File path + public void PrintGraph(string pathToFile); + + /// + /// Function for checking the connectivity of a graph + /// + /// Is the graph connected + public bool IsTheGraphConnected(); +} \ No newline at end of file diff --git a/Routers/Routers/Routers/Routers.cs b/Routers/Routers/Routers/Routers.cs new file mode 100644 index 0000000..8882f51 --- /dev/null +++ b/Routers/Routers/Routers/Routers.cs @@ -0,0 +1,290 @@ +namespace Routers; + +using System; +using System.Collections.Generic; + +/// +/// Class representing a graph +/// +public class Graph : IGraph +{ + /// + /// Class for storing graph vertices + /// + private class Node + { + // Useless dictionary needed to output a table to a file by condition + public Dictionary newDictionary = new(); + } + + public Graph(){ } + + public Graph (int x) + { + AddNode(x); + } + + /// List for storing vertices + private readonly List nodes = new(); + + /// List for storing vertex values + /// nodes[0] corresponds to nodeValue[0] + private readonly List nodeValue = new(); + + /// Dictionary of Adjacency + /// The key is a pair of vertices, the value is the length of the edge between them + private Dictionary<(int, int), int> adjacencyDictionary = new(); + + /// + /// Function for adding a node + /// + /// Node value + /// Was there a value in the graph + public bool AddNode(int index) + { + if (!nodeValue.Contains(index)) + { + nodes.Add(new()); + nodeValue.Add(index); + return true; + } + + return false; + } + + /// + /// Function for creating an edge between vertices + /// + /// Value of the first node + /// Value of the second node + /// Edge length + /// Was there an edge in the graph + public bool SetLength(int valueFirstNode, int valueSecondNode, int length) + { + if (!nodeValue.Contains(valueFirstNode) || !nodeValue.Contains(valueSecondNode) || valueSecondNode == valueFirstNode) + { + return false; + } + + if (adjacencyDictionary.ContainsKey((valueFirstNode, valueSecondNode))) + { + adjacencyDictionary.Remove((valueFirstNode, valueSecondNode)); + } + + adjacencyDictionary.Add((valueFirstNode, valueSecondNode), length); + return true; + } + + /// + /// Function for constructing a new graph without cycles + /// + /// Acyclic graph, with maximum sum of edges + public Graph BuildAcyclicGraph() + { + if (!IsTheGraphConnected()) + { + throw new DisconnectedGraph(); + } + + // List of subgraphs, each of which initially contains a vertex from the original graph + List subgraphs = new(); + + // The keys are nodes, the value is the index of the graph with the list in which the node is located + Dictionary graphInWhichTheNodeIsLocated = new(); + + for (int i = 0; i < nodeValue.Count; i++) + { + subgraphs.Add(new(nodeValue[i])); + graphInWhichTheNodeIsLocated.Add(nodeValue[i], i); + } + + ///Dictionary of the adjacency of the original graph, but with values sorted in descending order + adjacencyDictionary = adjacencyDictionary.OrderBy(x => x.Value, new Comparator()).ToDictionary(x => x.Key, x => x.Value); + + /// We will go by the values in the adjacency dictionary. + /// If two vertices are in different graphs, then we combine + /// them into one graph by combining all existing vertices and adding a new edge. + /// If two vertices already lie in the same graph, + /// then adding an edge with the same vertices will result in a cycle, so in this case we do nothing. + /// Eventually there will remain one acyclic graph with the maximum sum of edges + while (subgraphs.Count != 1) + { + // For each pair of vertices from the adjacency dictionary + foreach ((int, int) edgeVertices in adjacencyDictionary.Keys) + { + var (firstNode, secondNode) = edgeVertices; + var length = adjacencyDictionary[edgeVertices]; + if (graphInWhichTheNodeIsLocated[firstNode] == graphInWhichTheNodeIsLocated[secondNode]) + { + continue; + } + + else + { + CombineTwoGraphs(firstNode, secondNode, length, subgraphs, graphInWhichTheNodeIsLocated); + } + } + } + + return subgraphs[0]; + } + + /// + /// Function for combining two graphs + /// + /// Value of the first node + /// Value of the second node + /// Edge length + /// List of graphs + private static void CombineTwoGraphs(int firstNode, int secondNode, int length, List subgraphs, Dictionary dictionary) + { + Graph firstGraph = subgraphs[dictionary[firstNode]]; + Graph secondGraph = subgraphs[dictionary[secondNode]]; + + for (int i = 0; i < firstGraph.nodeValue.Count; i++) + { + secondGraph.AddNode(firstGraph.nodeValue[i]); + } + + secondGraph.adjacencyDictionary = + firstGraph.adjacencyDictionary.Concat(secondGraph.adjacencyDictionary).ToDictionary(x => x.Key, x => x.Value); + secondGraph.adjacencyDictionary.Add((firstNode, secondNode), length); + + firstGraph.nodes.Clear(); + firstGraph.nodeValue.Clear(); + firstGraph.adjacencyDictionary.Clear(); + subgraphs.Remove(firstGraph); + } + + /// + /// useless function designed to create a new dictionary for easy output to a file + /// + private void ConvertToAnotherDictionary() + { + foreach ((int, int) h in adjacencyDictionary.Keys) + { + var (first, second) = h; + nodes[nodeValue.IndexOf(first)].newDictionary.Add(second, adjacencyDictionary[h]); + } + } + + private List NumberOfConnectedGraphElements(int nodeIndex, List isTerminal) + { + isTerminal.Add(nodeIndex); + var array = new List(); + foreach ((int, int) h in adjacencyDictionary.Keys) + { + var (first, second) = h; + if (first == nodeIndex) + { + array.Add(second); + } + else if (second == nodeIndex) + { + array.Add(first); + } + } + + foreach (int item in array) + { + if (!isTerminal.Contains(item)) + { + NumberOfConnectedGraphElements(item, isTerminal); + } + } + + return isTerminal; + } + + /// + /// Function for checking the connectivity of a graph + /// + /// Is the graph connected + public bool IsTheGraphConnected() + => nodeValue.Count == 0 || NumberOfConnectedGraphElements(nodeValue[0], new()).Count == nodes.Count; + + /// + /// Function for graph constructionПуть + /// + /// The path to the file with the table + public void BuildGraph(string pathToFile) + { + var stringToConvert = File.ReadAllLines(pathToFile); + for (int i = 0; i < stringToConvert.Length; i++) + { + var newStringArray = stringToConvert[i].Split(' '); + if (Int32.TryParse(newStringArray[0], out int firstNumber)) + { + AddNode(firstNumber); + } + + int secondNumber = 0; + for (int j = 1; j < newStringArray.Length; j++) + { + if (newStringArray[j] == ":") + { + continue; + } + + if (Int32.TryParse(newStringArray[j], out int number)) + { + AddNode(number); + secondNumber = number; + } + + else + { + int k = 0; + string newNumber = ""; + while (newStringArray[j][k] != ')') + { + if (newStringArray[j][k] == '(' || newStringArray[j][k] == ',') + { + k++; + continue; + } + + newNumber += newStringArray[j][k]; + k++; + } + + if (Int32.TryParse(newNumber, out int length)) + { + SetLength(firstNumber, secondNumber, length); + } + } + } + } + } + + /// + /// Function for graph printing + /// + /// File path + public void PrintGraph(string pathToFile) + { + ConvertToAnotherDictionary(); + using StreamWriter writer = new(pathToFile); + { + for (int i = 0; i < nodeValue.Count; i++) + { + if (nodes[i].newDictionary.Keys.Count != 0) + { + writer.Write($"{nodeValue[i]} : "); + } + + else + { + continue; + } + + foreach (int key in nodes[i].newDictionary.Keys) + { + writer.Write($"{key} ({nodes[i].newDictionary[key]}), "); + } + + writer.WriteLine(); + } + } + } +} diff --git a/Routers/Routers/Routers/Routers.csproj b/Routers/Routers/Routers/Routers.csproj new file mode 100644 index 0000000..74abf5c --- /dev/null +++ b/Routers/Routers/Routers/Routers.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/Routers/Routers/Routers/Solution.cs b/Routers/Routers/Routers/Solution.cs new file mode 100644 index 0000000..0508cf3 --- /dev/null +++ b/Routers/Routers/Routers/Solution.cs @@ -0,0 +1,35 @@ +namespace Routers; + +public class Solution +{ + static int Main(string[] args) + { + if (args.Length != 2) + { + Console.WriteLine("Invalid input arguments"); + return -1; + } + + IGraph graph = new Graph(); + graph.BuildGraph(args[0]); + Graph? newGraph; + + try + { + newGraph = graph.BuildAcyclicGraph(); + } + catch (DisconnectedGraph exception) + { + Console.WriteLine($"Error: {exception.Message}"); + return -1; + } + + if (newGraph == null) + { + return -1; + } + + newGraph.PrintGraph(args[1]); + return 0; + } +}