Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get all non-overlapping permutations for a series of time blocks?

Tags:

c#

.net

algorithm

I have what seems to be a simple problem that I am having a hard time modeling in code (C#) -

I am trying to find the highest potential credit hours available to a person attending a conference. Courses have time blocks, such as Security 101 @ 9AM-10AM, Finance 202 @ 4PM-6PM, etc.

The main rule is, you can't attend two courses at once - so you would get credit for courses at 9-10 and 10-11, but you could not also get credit for a course that ran for 9-11.

What I would like to do is the following:

I would like to get an array of valid (valid meaning non-overlapping) paths throughout a day.

So, for example, the full set of courses for a day may be the following:

|---------------------------------------------------|
| COURSE            |   START       |   END         |
|-------------------|---------------|---------------|
| FINANCE 101       |   9:00 AM     |   10:00 AM    |
| FINANCE 102       |   10:00 AM    |   11:00 AM    |
| PYTHON 300        |   10:00 AM    |   11:00 AM    |
| SECURITY 101      |   11:00 AM    |   12:00 PM    |
| ECONOMICS 101     |   9:00 AM     |   12:00 PM    |
| DATABASE 200      |   11:00 AM    |   1:00 PM     |
|---------------------------------------------------|

There are a few paths someone might take throughout this day:

  • FINANCE 101 (9-10) -> FINANCE 102 (10-11) -> SECURITY 101 (11-12) -> DONE
  • FINANCE 101 (9-10) -> PYTHON 300 (10-11) -> SECURITY 101 (11-12) -> DONE
  • FINANCE 101 (9-10) -> FINANCE 102 (10-11) -> DATABASE 200 (11-1) -> DONE
  • FINANCE 101 (9-10) -> PYTHON 300 (10-11) -> DATABASE 200 (11-1) -> DONE
  • ECONOMICS 101 (9-12)-> DONE

This is a somewhat simple scenario, in reality it would be possible to have multiple branching scenarios, such as having three 9-10 courses that would create more permutations on top of this.

The reason I would like an array of paths (instead of one single optimal path) is because there isn't necessarily a direct 1 Hour = 1 Credit Hour correlation, there would be a second level calculation based on the set of paths to sum the credit hour value of the path to determine what is 'best'.

My question is this - is there a technique or software pattern that I can follow in order to generate these permutations so that I can measure the results to determine the path that would yield the most credits for a course-taker?

Edited for Solution:

Thanks everyone for your input and help, both solutions from Bradley Uffner and Xiaoy312 nailed it!

enter image description here

like image 677
Evan Avatar asked Sep 01 '17 18:09

Evan


2 Answers

Answer adapted from Ordered Permutation of List<Int>:

public static class CourseExtensions
{    
    public static IEnumerable<IEnumerable<Course>> GetPermutations(this IEnumerable<Course> courses)
    {
        return GetCoursesHelper(courses, TimeSpan.Zero);
    }
    private static IEnumerable<IEnumerable<Course>> GetCoursesHelper(IEnumerable<Course> courses, TimeSpan from)
    {        
        foreach (var course in courses)
        {
            if (course.Start < from) continue;

            yield return new[] { course };

            var permutations = GetCoursesHelper(courses, course.End);
            foreach (var subPermutation in permutations)
            {
                yield return new[]{ course }.Concat(subPermutation);
            }
        }
    }
}

Full code:

void Main()
{
    foreach (var courses in GetCourses().GetPermutations())
    {
        Console.WriteLine(string.Join(" -> ", courses
            .Select(x => x.ToString())
            .Concat(new [] { "DONE" })));
    }
}

// Define other methods and classes here
public class Course
{
    public string Name { get; set; }
    public TimeSpan Start { get; set; }
    public TimeSpan End { get; set; }

    public override string ToString()
    {
        return string.Format("{0} ({1:hhmm}-{2:hhmm})",
           Name, Start, End);
    }
}

IEnumerable<Course> GetCourses() 
{
    var data = @"
| FINANCE 101       |   9:00 AM     |   10:00 AM    |
| FINANCE 102       |   10:00 AM    |   11:00 AM    |
| PYTHON 300        |   10:00 AM    |   11:00 AM    |
| SECURITY 101      |   11:00 AM    |   12:00 PM    |
| ECONOMICS 101     |   9:00 AM     |   12:00 PM    |
| DATABASE 200      |   11:00 AM    |   1:00 PM     |
".Trim();

    return data.Split('\n')
        .Select(r => r.Split('|').Select(c => c.Trim()).ToArray())
        .Select(x => new Course
        {
            Name = x[1],
            Start = DateTime.ParseExact(x[2], "h:mm tt", CultureInfo.InvariantCulture).TimeOfDay,
            End = DateTime.ParseExact(x[3], "h:mm tt", CultureInfo.InvariantCulture).TimeOfDay
        });
}

public static class CourseExtensions
{    
    public static IEnumerable<IEnumerable<Course>> GetPermutations(this IEnumerable<Course> courses)
    {
        return GetCoursesHelper(courses, TimeSpan.Zero);
    }
    private static IEnumerable<IEnumerable<Course>> GetCoursesHelper(IEnumerable<Course> courses, TimeSpan from)
    {        
        foreach (var course in courses)
        {
            if (course.Start < from) continue;

            yield return new[] { course };

            var permutations = GetCoursesHelper(courses, course.End);
            foreach (var subPermutation in permutations)
            {
                yield return new[]{ course }.Concat(subPermutation);
            }
        }
    }
}

Output:

FINANCE 101 (0900-1000) -> DONE
FINANCE 101 (0900-1000) -> FINANCE 102 (1000-1100) -> DONE
FINANCE 101 (0900-1000) -> FINANCE 102 (1000-1100) -> SECURITY 101 (1100-1200) -> DONE
FINANCE 101 (0900-1000) -> FINANCE 102 (1000-1100) -> DATABASE 200 (1100-1300) -> DONE
FINANCE 101 (0900-1000) -> PYTHON 300 (1000-1100) -> DONE
FINANCE 101 (0900-1000) -> PYTHON 300 (1000-1100) -> SECURITY 101 (1100-1200) -> DONE
FINANCE 101 (0900-1000) -> PYTHON 300 (1000-1100) -> DATABASE 200 (1100-1300) -> DONE
FINANCE 101 (0900-1000) -> SECURITY 101 (1100-1200) -> DONE
FINANCE 101 (0900-1000) -> DATABASE 200 (1100-1300) -> DONE
FINANCE 102 (1000-1100) -> DONE
FINANCE 102 (1000-1100) -> SECURITY 101 (1100-1200) -> DONE
FINANCE 102 (1000-1100) -> DATABASE 200 (1100-1300) -> DONE
PYTHON 300 (1000-1100) -> DONE
PYTHON 300 (1000-1100) -> SECURITY 101 (1100-1200) -> DONE
PYTHON 300 (1000-1100) -> DATABASE 200 (1100-1300) -> DONE
SECURITY 101 (1100-1200) -> DONE
ECONOMICS 101 (0900-1200) -> DONE
DATABASE 200 (1100-1300) -> DONE
like image 78
Xiaoy312 Avatar answered Nov 14 '22 23:11

Xiaoy312


This will just recursively walk through the list of courses, picking any courses that start on or after the end of the last course taken.

It probably isn't as efficient as @Xiaoy312's answer, but it shows another method. I've also added course credits, displaying the total credit for a particular path, as well as selecting the optimal path.

This could be cleanup up significantly by adding a proper CourseLoad class to store the class list instead of using List<> and List<List<>>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace CoursePath
{
    class Program
    {
        static void Main(string[] args)
        {
            var courses = new List<CourseInfo>()
                          {
                              new CourseInfo("Finance 101", 1, DateTime.Parse("9:00 AM"), DateTime.Parse("10:00 AM")),
                              new CourseInfo("Finance 102", 2, DateTime.Parse("10:00 AM"), DateTime.Parse("11:00 AM")),
                              new CourseInfo("Python 300", 3, DateTime.Parse("10:00 AM"), DateTime.Parse("11:00 AM")),
                              new CourseInfo("Security 101", 4, DateTime.Parse("11:00 AM"), DateTime.Parse("12:00 PM")),
                              new CourseInfo("Economics 201", 5, DateTime.Parse("9:00 AM"), DateTime.Parse("12:00 PM")),
                              new CourseInfo("Database 200", 6, DateTime.Parse("11:00 AM"), DateTime.Parse("1:00 PM"))
                          };

            var results = new List<List<CourseInfo>>();

            BuildCourseList(null, courses, results);

            results.ForEach(c => Console.WriteLine(string.Join(" -> ", c.Select(x => x.Name)) + $" -> Done ({c.Sum(x => x.Credits)} credits)"));
            Console.WriteLine();
            var optimal = results.Select(path => new {Path = path, TotalCredits = path.Sum(c => c.Credits)}).OrderByDescending(path => path.TotalCredits).First();
            Console.WriteLine("Optimal Path: " + string.Join(" -> ", optimal.Path.Select(x => x.Name)) + $" -> Done ({optimal.TotalCredits} credits)");
            Console.Read();
        }

        public static void BuildCourseList(List<CourseInfo> currentPath, List<CourseInfo> courses, List<List<CourseInfo>> results)
        {
            CourseInfo currentCourse = currentPath?.LastOrDefault();
            var candidates = (currentCourse == null ? courses : courses.Where(c => c.StarTime >= currentCourse.EndTime));
            if (currentPath != null)
            {
                results.Add(currentPath);
            }
            foreach (var course in candidates)
            {
                var nextPath = currentPath == null ? new List<CourseInfo>() : new List<CourseInfo>(currentPath);
                nextPath.Add(course);
                BuildCourseList(nextPath, courses, results);
            }
        }
    }

    public class CourseInfo
    {
        public CourseInfo(string name, int credits, DateTime starTime, DateTime endTime)
        {
            Name = name;
            Credits = credits;
            StarTime = starTime;
            EndTime = endTime;
        }

        public string Name { get; set; }
        public int Credits { get; set; }
        public DateTime StarTime { get; set; }
        public DateTime EndTime { get; set; }
    }
}

Output:

Finance 101 -> Done (1 credits)
Finance 101 -> Finance 102 -> Done (3 credits)
Finance 101 -> Finance 102 -> Security 101 -> Done (7 credits)
Finance 101 -> Finance 102 -> Database 200 -> Done (9 credits)
Finance 101 -> Python 300 -> Done (4 credits)
Finance 101 -> Python 300 -> Security 101 -> Done (8 credits)
Finance 101 -> Python 300 -> Database 200 -> Done (10 credits)
Finance 101 -> Security 101 -> Done (5 credits)
Finance 101 -> Database 200 -> Done (7 credits)
Finance 102 -> Done (2 credits)
Finance 102 -> Security 101 -> Done (6 credits)
Finance 102 -> Database 200 -> Done (8 credits)
Python 300 -> Done (3 credits)
Python 300 -> Security 101 -> Done (7 credits)
Python 300 -> Database 200 -> Done (9 credits)
Security 101 -> Done (4 credits)
Economics 201 -> Done (5 credits)
Database 200 -> Done (6 credits)

Optimal Path: Finance 101 -> Python 300 -> Database 200 -> Done (10 credits)
like image 44
Bradley Uffner Avatar answered Nov 14 '22 23:11

Bradley Uffner