0

I am creating a program that optimises the classes provided by my university for student's timetables.

I parse data into the following structs/classes:

public struct Subject
{  
    public int Classes {...}
    public int Hours { ... }
    public string Name { get; set; }
    public string Code { get; set; }
    public string Base_Code;
    public Campus Campus { get; set; }
    public Semester Semester;  
}

public struct Timeslot
{
    public string ClassType; //code for type: i.e. L for Lecture, W for Workshop
    public string Description;
    public DayOfWeek Day;
    public DateTime Start, Finish;
    public List<Tuple<string, string>> weekSpans;
    public string Location;
}

public struct Class
{
    public Subject subject { get; set; }
    public Timeslot time { get; set; }
}

Across my n subjects for the semester, I have a total of m mandatory classes to attend (lecture 1, 2, 3, workshop e.t.c). I have these stored in a list of list of type 'Class'.

ex:

Lecture 1 (subject 1)

  • class @ 9am Monday
  • class @ 1pm Monday

Workshop 1 (subject 2)

  • class @ 9am Monday
  • class @ 2pm Tuesday

I am currently using Eric Lippert's Cartesian Product method to generate a IEnumerable<IEnumerable<Class>> consisting of all n-class combinations.

static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences) 
{ 
  // base case: 
  IEnumerable<IEnumerable<T>> result = new[] { Enumerable.Empty<T>() }; 
  foreach(var sequence in sequences) 
  { 
    var s = sequence; // don't close over the loop variable 
    // recursive case: use SelectMany to build the new product out of the old one 
    result = 
      from seq in result 
      from item in s 
      select seq.Concat(new[] {item}); 
    } 
  return result; 
}

However, my issue occurs when I need to gather necessary information from each IEnumerable to determine which timetable fits set criteria (shorter days, most days off, e.t.c.). Trying to iterate through all and storing them in my TimeTable container class is really taxing to the memory on top of the memory needed to store the initial permutations.

public class TimeTable
{
    public IEnumerable<Class> _classes;
    public DateTime MinStart { get; private set; }
    public DateTime MaxFinish { get; private set; }
    public int DaysOff { get; private set; }
    public TimeTable(IEnumerable<Class> Classes) { ... }
}

It is so taxing in fact that my memory skyrockets to 2GB and then crashes. I have thought of optimizing the permutation process by discarding timetables that include clashes, which I know how to do but I cannot figure out where I can put my code into the CartesianProduct algorithm so that it isn't kept in memory. I have also thought of assigning each class an ID and then calculating the permutations however that doesn't optimise the need to sort the timetables by the user's preference.

This is my first personal project where I have to deal with combinatorics and memory optimisation so any advice and help on how I can optimise this optimisation process (ha!) would be much appreciated.

Trontor
  • 417
  • 1
  • 7
  • 20
  • None of those types should be structs. They're mutable, too big for a struct, and don't logically represent a single value. They should all be classes. – Servy Feb 08 '18 at 15:57
  • You simply *aren't* going to be able to store all of that in memory. You need to figure out how to find the information you want while only ever having a constant number of permutations in memory at any one time. Permutations simply grow too quickly for you to do anything else. – Servy Feb 08 '18 at 15:58
  • In any but the smallest scheduling problems we don't want to generate all possible permutations. You see why this is the case. Often these problems are attacked by Mixed Integer Programming, Constraint Programming or some form of (meta-) heuristic. – Erwin Kalvelagen Feb 08 '18 at 23:37

1 Answers1

0

You needn't store all element combinations in a big super collection, to iterate over them. I once wrote a java solution, which should be translateable to c# easily, but might not fit exactly, but the main idea, I can layout here.

You have some mandatory courses to combine, let's assume a1, a2, a3; b1, b2; c1, c2, c3, c4.

The student has to take one of a, one of b, one of c.

The number of possible combinations is a.size * b.size * c.size = 3*2*4 = 24.

So instead of generating the combinations and iterating over them, you just loop or iterate with int i over the numbers 0 to 23, and pick a course combination, which would be at index i, if you had build the combinations.

How is it done? Well, assume the number i is 17. i % (c.size) = 17%4= 1. This would be the second (0, 1, 2, 3) element of c, c2.

Now we divide i' (i % c.size) by c.size and get 17/4=4. 4 % b.size(2) = 0 so for b we get index=0 which is b1.

The i' is divided by b.size which is i"=2. At index 2 (% (3 = a.size)) we find a3. For the last element we wouldn't need a modulo, but on the other side, it does no harm and allows for a uniform process.

Try it with small numbers to convince yourself, that all combinations occur exactly and reliably once and in a systematic order.

If two courses clash, just take the next one.

user unknown
  • 35,537
  • 11
  • 75
  • 121
  • I see the logic behind it, but does this allow me to be able to sort the permutations that have no clashes? – Trontor Feb 11 '18 at 06:01
  • Since there is no state to hold a collection of elements, not directly: Each combination is only generated JIT, but you can filter the generation and store the results in your own collection. Iterate like in the CartesianIteratorTest, `for (List lo: ci) if filter (lo) myList.add (lo);` and filter.lo could be an predicate like (notOverlapping (lo(0).time , lo(1).time)), assuming lo(0) and lo (1) are curses and curses.time are intervals, for which you can define a method (notOverlapping (t1, t2). – user unknown Feb 11 '18 at 11:04
  • Since there are more than 2 Elements to compare, it would not be the comparison of 2 but each pair of Elements. In Pseudocode: for a<-lo; b<-lo; if (a != b) notOverlapping (a.time, b.time); This should reduce the number of combinations, surviving the filter, drastically which might allow to store them all without problems - depends on problem size and storage possibilities of course. If problems of storage would occur, you could just store the index instead, because with the index, the collection is always reproducable. – user unknown Feb 11 '18 at 11:10
  • This method works, I think. An issue I faced is when there is only 1 item in a List for say, a 1-time only lecture for during the week. If this was at the top of List> then it would result in performing x % 1, yielding 0 everytime. I had to order by descending by list count for it to work. – Trontor Feb 12 '18 at 15:20
  • @Trontor: That might be an implementation problem, I guess. My reference implementation here https://stackoverflow.com/a/10083452/312172 , if I change one of the lists to contain just one element, works fine, as it should. But your courses shall not conflict with each other, and every course will conflict by time overlap with this full week course - is that the problem? Maybe you need an empty-course as alternative in each list, if it is possible to prolonge the topic to the next semester? – user unknown Feb 12 '18 at 16:55
  • By your instructions, if I have i = 17 then i' = i % c.Size. If c = { c1 } then c.size = 1, so i' = 17 % 1 = 0. From there you say do i' / c.zize / b. size, but size i' = 0, every index chosen by this method from thereon results in a 0 index, right? – Trontor Feb 13 '18 at 01:19