Another graph algorithm, this time to create a priority or build order. Provided with a starting List<Project>
and List<ProjectWithPrerequisite>
the algorithm will return the order to build them in. For a list of projects a, b, c, d, e, f
and their corresponding prerequisites where a, d
means that a is a prerequisite for d or a-->d
.
To find the build order, projects are sorted in descending prerequisite order so that projects with the most prerequisites come first. Each project has a path created for every prerequisite up until the starting node is found, which has no prerequisites. Projects with multiple prerequisites, and subsequent multiple paths, have these paths merged into a single path for the build order of that project. Once the linear path has been created for the project it is added to a completed order list.
To avoid repeatedly searching the same path I check if a Project
already belongs to the completed order and if so stop checking since it will already have its itself and precedents as members.
I have not considered the scenarios where:
- All projects have prerequisites which forms a loop with itself. A-->B-->C-->A
- There are two or more non-connected paths (islands) for the same project.
Included at the end are the unit tests I used.
How can I improve my logic? Does anything stand out as excessively complex or not straightforward enough?
public class Project
{
public List<Project> Prerequisites { get; } = new List<Project>();
public char Name { get; }
public Project(char name)
{
Name = name;
}
}
public class ProjectWithPrerequisite
{
public Project Project { get; }
public Project Prerequisite { get; }
public ProjectWithPrerequisite(Project prerequisite, Project project)
{
Prerequisite = prerequisite;
Project = project;
}
}
public class ProjectBuildOrder
{
private Dictionary<char, Project> _projects { get; }
private List<ProjectWithPrerequisite> _singlePrerequisites { get; }
private List<Project> _completedOrder = new List<Project>();
public ProjectBuildOrder(List<Project> projects, List<ProjectWithPrerequisite> singlePrerequisites)
{
_projects = new Dictionary<char, Project>(projects.Count);
foreach (var item in projects)
{
_projects.Add(item.Name, item);
}
_singlePrerequisites = singlePrerequisites;
}
/// <summary>
/// Creates the build order to accomplish the given list of projects.
/// </summary>
/// <returns></returns>
public List<Project> GenerateBuildOrder()
{
AddPrerequisitesToProjects();
return BuildOrder();
}
/// <summary>
/// Adds the provided prerequisites to the projects.
/// </summary>
private void AddPrerequisitesToProjects()
{
foreach (var pair in _singlePrerequisites)
{
var projectWithPrerequisite = _projects[pair.Project.Name];
projectWithPrerequisite.Prerequisites.Add(pair.Prerequisite);
}
}
/// <summary>
/// Creates the build order for the list of <see cref="Project"/>s.
/// </summary>
/// <returns><see cref="List{T}"/> containing the build order for the provided list of <see cref="Project"/>s and their prerequisites.</returns>
private List<Project> BuildOrder()
{
var checkOrder = _projects
.OrderByDescending(kvp => kvp.Value.Prerequisites.Count).Select(kvp => kvp.Value);
_completedOrder = new List<Project>();
var path = new LinkedList<Project>();
foreach (var project in checkOrder.Where(p => !_completedOrder.Contains(p)))
{
if (project.Prerequisites.Count > 1)
{
var branchPaths = GetBranchPrecedents(project);
path = MergePaths(branchPaths);
}
else
{
path = NonBranchingPath(project);
}
_completedOrder.AddRange(path.Where(p => !_completedOrder.Contains(p)));
}
return _completedOrder;
}
/// <summary>
/// For a node which has only a single prerequisite. This will follow the path back to the end, branching if necessary by claling <see cref="GetBranchPrecedents(Project)"/>.
/// </summary>
/// <param name="project">The node whose precedents will be listed.</param>
/// <returns></returns>
private LinkedList<Project> NonBranchingPath(Project project)
{
if (project.Prerequisites.Count == 0)
{
var ll = new LinkedList<Project>();
ll.AddLast(project);
return ll;
}
if (project.Prerequisites.Count == 1)
{
var ll = new LinkedList<Project>();
ll.AddLast(project);
var parent = project.Prerequisites[0];
if (_completedOrder.Contains(parent))
{
return ll;
}
while (parent.Prerequisites.Count == 1)
{
ll.AddFirst(parent);
parent = parent.Prerequisites[0];
if (_completedOrder.Contains(parent))
{
break;
}
}
if (parent.Prerequisites.Count == 0)
{
if (!_completedOrder.Contains(parent))
{
ll.AddFirst(parent);
}
return ll;
}
var parentPath = MergePaths(GetBranchPrecedents(parent));
var first = ll.First.Value;
ll.RemoveFirst();
parentPath.AddLast(first);
return parentPath;
}
return MergePaths(GetBranchPrecedents(project));
}
/// <summary>
/// When a node contains multiple prerequisites it will follow each path. If a prerequisite path branches it will recursively
/// call itself to find those branching paths, and merging them.
/// </summary>
/// <param name="projectForPrerequisite">Node containini more than one prerequisite.</param>
/// <returns><see cref="List{T}"/> containing the distinct path branches.</returns>
private List<LinkedList<Project>> GetBranchPrecedents(Project projectForPrerequisite)
{
var list = new List<LinkedList<Project>>();
foreach (var parent in projectForPrerequisite.Prerequisites.Where(project => !_completedOrder.Contains(project)))
{
switch (parent.Prerequisites.Count)
{
case 0:
var endOfPrecedenceChain = new LinkedList<Project>();
endOfPrecedenceChain.AddFirst(parent);
endOfPrecedenceChain.AddLast(projectForPrerequisite);
list.Add(endOfPrecedenceChain);
break;
case 1:
var nonBranch = NonBranchingPath(parent);
nonBranch.AddLast(projectForPrerequisite);
list.Add(nonBranch);
break;
default:
var branchPrecedents = GetBranchPrecedents(parent);
var mergedPrecedents = MergePaths(branchPrecedents);
mergedPrecedents.AddLast(parent);
mergedPrecedents.AddLast(projectForPrerequisite);
list.Add(mergedPrecedents);
break;
}
}
return list;
}
/// <summary>
/// Merge each of the branching paths in the <see cref="LinkedList{T}"/> into one. Merging based on precedence they were added.
/// </summary>
/// <param name="paths">A <see cref="List{T}"/> containing the branching paths.</param>
/// <returns><see cref="LinkedList{T}"/> of path back to a starting node which has no prerequisites.</returns>
LinkedList<Project> MergePaths(List<LinkedList<Project>> paths)
{
if (paths.Count == 1)
{
return paths[0];
}
var last = paths[0].Last.Value;
var merged = paths[0];
merged.RemoveLast();
LinkedList<Project> ll;
for (int path = 1; path < paths.Count; path++)
{
ll = paths[path];
ll.RemoveLast();
while (ll.Any())
{
if (!merged.Contains(ll.First.Value))
{
merged.AddLast(ll.First.Value);
}
ll.RemoveFirst();
}
}
merged.AddLast(last);
return merged;
}
}
Unit tests to check results against.
[Fact]
public void Single_branch_list_follows_build_order()
{
#region All_projects
var a = new Project('a');
var b = new Project('b');
var c = new Project('c');
var d = new Project('d');
var e = new Project('e');
var f = new Project('f');
#endregion
var expected = new List<Project>() { f, a, b, d, c, e };
var projects = new List<Project>() { a, b, c, d, e, f };
var projectsAndPrerequisite = new List<ProjectWithPrerequisite>()
{
new ProjectWithPrerequisite(a, d),
new ProjectWithPrerequisite(f, b),
new ProjectWithPrerequisite(b, d),
new ProjectWithPrerequisite(f, a),
new ProjectWithPrerequisite(d, c)
};
var sut = new ProjectBuildOrder(projects, projectsAndPrerequisite);
var actual = sut.GenerateBuildOrder();
Assert.Equal(expected, actual);
}
[Fact]
public void Multi_branch_list_follows_build_order()
{
#region All_projects
var a = new Project('a');
var b = new Project('b');
var c = new Project('c');
var d = new Project('d');
var e = new Project('e');
var f = new Project('f');
var g = new Project('g');
#endregion
var expected = new List<Project>() { g, f, a, b, d, c, e };
var projects = new List<Project>() { a, b, c, d, e, f, g };
var projectsAndPrerequisite = new List<ProjectWithPrerequisite>()
{
new ProjectWithPrerequisite(g, c),
new ProjectWithPrerequisite(a, d),
new ProjectWithPrerequisite(f, b),
new ProjectWithPrerequisite(b, d),
new ProjectWithPrerequisite(f, a),
new ProjectWithPrerequisite(d, c)
};
var sut = new ProjectBuildOrder(projects, projectsAndPrerequisite);
var actual = sut.GenerateBuildOrder();
Assert.Equal(expected, actual);
}
[Fact]
public void Multi_branch_list_has_prerequisites_sorted_in_alphabetical_order()
{
#region All_projects
var a = new Project('a');
var b = new Project('b');
var c = new Project('c');
var d = new Project('d');
var e = new Project('e');
var f = new Project('f');
var g = new Project('g');
#endregion
var expected = new List<Project>() { f, g, b, a, d, c, e };
var projects = new List<Project>() { a, b, c, d, e, f, g };
var projectsAndPrerequisite = new List<ProjectWithPrerequisite>()
{
new ProjectWithPrerequisite(g, b),
new ProjectWithPrerequisite(g, c),
new ProjectWithPrerequisite(a, d),
new ProjectWithPrerequisite(f, b),
new ProjectWithPrerequisite(b, d),
new ProjectWithPrerequisite(f, a),
new ProjectWithPrerequisite(d, c),
new ProjectWithPrerequisite(f, g),
};
var sut = new ProjectBuildOrder(projects, projectsAndPrerequisite);
var actual = sut.GenerateBuildOrder();
Assert.Equal(expected, actual);
}