Mike Ross
Mike Ross

Reputation: 2972

Event entries scheduling algorithm PHP

we have an entry portal system in which we accept entries for events.

For example Championship Event 2017 will be held on 30th Nov.

Event got about 150 entries in different classes. Junior Class, Senior Class, Pro Class etc.

Now event venue only has certain numbers of ground on which the competition can be held. For example Ground 1, Ground 2 and Ground 3. Its a solo performance event.

Now our system needs to generate a schedule in such a way that competitors who entered multiple classes or same classes multiple times should get maximum break between their performances.

The input data we have are registration under each class. Starting time of each Ground. For example Ground A will start at 8:00 AM, Ground 2 at 8:00 and Ground 3 at 9:00. We also know that which class will be held in which arena. For example Junior and senior Class will be held in Ground 1 and Pro Class will be held in Ground 2.

We know the performance time as well. Senior Class 1 performance is 5 minutes. Junior class performance is 7 minutes and Pro Performance is 9 minutes.

Now I have written following code to get the schedule so that competitors competing multiple times in one class or in multiple class get maximum break between their performance but it still puts same competitor performance one after another.

Let me know what is my mistake.

foreach ($totalPerformanceTimeSlot as $time => $performance) {
    # $totalPerformanceTimeSlot is array of timeslots starting from 8:00 am

    foreach ($performance as $classId) {
        #there could be 2 performance at the same time in different arena for different class.
        $totalPerformanceLeftThisClass = count($this->lassRegistrationLinks[$classId]); //Get the total performance for this class from array;

        # $accountRidesLeftArray has value of how many times each account is performing in this class
        arsort($accountRidesLeftArray);
        # for each person,  estimate what their start time threshold should be based on how many times they're performing
        $accountPerformanceTimeThreshold = array();

        foreach ($accountPerformanceLeftArray as $accountId => $accountPerformancesLeft) {

            $tempPerformanceThreshold = 20 * 60;
            # reduce this person's performance threshold by a performance at a time until the minimum performance threshold has been met
            while ((($totalPerformanceLeftThisClass * $this->classes[$classId]['performanceTime']) / $accountPerformanceLeft < $tempPerformanceThreshold) && ($tempPerformanceThreshold > $this->minRideThreshold))
                $tempPerformanceThreshold -= $this->classes[$classId]['performanceTime'];

            $accountPerformanceTimeThreshold[$accountId] = $tempPerformanceThreshold;
        }

        $performanceLeft = $totalPerformanceLeftThisClass - $count;

        # given the number of performance left in the class,
        # calculate how important it is per account that they get placed in the next slot
        $accountToPerformNextImportanceArray = array();
        $timeLeft = $performanceLeft * $this->classes[$classId]['performanceTime'];

        foreach ($accountPerformanceLeftArray as $accountId => $accountPerformancesLeft) {

            # work out the maximum number that can be used as entropy
            $entropyMax = (20 * 60 / ($timeLeft / 1)) * 0.5;
            $entropy = ((mt_rand (0, $entropyMax * 1000)) / 1000);

            # the absolute minimum amount of time required for this user to perform
            $minTimeRequiredForComfortableSpacing = ($accountRidesLeft - 1) * 20* 60;
            # add a bit of time around the absolute minimum amount of time required for this person to perform so that it doesn't instantly snap in when this person suddenly has the minimum amount of time left to perform
            $generalTimeRequiredForComfortableSpacing = $minTimeRequiredForComfortableSpacing * 1.7;

            $nearestPerformancePrior = $this->nearest_performance_prior($classDetails['date'], $currentTime, $accountId);
            $nearestRideAfter = $this->nearest_performance_after($classDetails['date'], $currentTime, $accountId);

           # work out how important it is for this rider to ride next based on how many rides they have left
           $importanceRating = 20 * 60 / ($timeLeft / $accountPerformanceLeft);

           # if there's more than enough time left then don't worry about giving this person any importance rating,  ie. it's not really important that they perform straight away
           if ($timeLeft > $generalTimeRequiredForComfortableSpacing)
                $importanceRating = 0;

           # add a little bit of random entropy to their importance rating
           $importanceRating += $entropy;

           # if this account has performed too recently to place them here in this slot,  then make them very undesirable for this slot
           if ((!is_null($nearestPerformancePrior)) && ($nearestPerformancePrior > $currentTime - $accountPerformanceTimeThreshold[$accountId]))
               $importanceRating = -1;

          # work out if this account will perform too soon afterwards to place them here in this slot,  then make them very undesirable for this slot
          if ((!is_null($nearestRideAfter)) && ($nearestRideAfter < $currentTime + $accountRideTimeThreshold[$accountId]))
               $importanceRating = -1;

           $accountToPerformNextImportanceArray[$accountId] = $importanceRating;

       }

       arsort($accountToPerformNextImportanceArray);
       //Then I take the first one from this array and allocate the time for that user.
       $this->set_performance_time($classDetails['date'], $accountId, $currentTime);
       $currentTime += $this->classes[$classId]['performanceTime'];
    }
}

Here is some explanation of the variables

$accountPerformancessLeft is total number of performance for each user.
For e.g. if user has entered into 2 classes that means $accountPerformancessLeft is 6 for that user.
threshold is something like break.
Rider and account is conceptually the same.

I know it is hard to think the output without the actual data but any help would be appreciated.

Thank you

Upvotes: 3

Views: 1263

Answers (2)

Alireza
Alireza

Reputation: 1038

Well, first let's see what we have and simplify the problem:

  • There are different competitions(events) but since they are independent to each other we can consider only one
  • We have C different classes (senior, junior, ...)
  • We have G different grounds that each ground may hold some of C classes.
  • There are some persons(competitor) lets say P who registers to C classes.
  • Persons need to have maximum possible break.

So putting them all together the problem is: The are some grounds G = {g1, g2, ..., gm} that each of them contains some persons P = {p1, p2, ..., pn}. We want to maximize the break time of each person in all of its competitions.

The trivial case:

First, let's assume that there is only one ground g1, and a group of person P = {p1, p2, ..., pn} who wants to compete on this ground. let's define a boolean method isItPossible(breaktime) showing that whether it is possible to schedule the competition that each person has at least breaktime to rest or not. we can simply prove that this method is monotonic i.e. if there exist a breaktime that isItPossible(breaktime) became true then:

isItPossible(t) = true       for every t <= breaktime

So we can use binary search to find the maximum value for breaktime. Here is the pseudo code (C++ syntax like):

double low = 0 , high = INF;
while(low < high){
    mid = (low + high) / 2;
    if(isItPossible(mid))
        low = mid;
    else
        high = mid;
}
breakTime = low;

Now the only thing remains is implementing isItPossible(breaktime) method. There are a lot of ways to implement it but I use greedy algorithm and a heap priority queue to solve it. We need a priority queue for maintaining some tuples. Each tuple contains a person, the number of time that person should compete and the earliest time we can schedule a competition for that person. We start from time t0 (the opening time of the ground e.g. it could be 8.00 a.m.) and each time we pick a person from the priority queue with the minimum earliest time. Here is the C++ like pseudo code:

bool isItPossible(double breaktime){
    //Tuple(personId, numberOfCompete, earliestTime)
    priority_queue<Tuple> pq;
    for p in Person_list
        pq.push(Tuple(p,countCompetition(p),t0));

    for(time = t0;time<end_of_ground_time;){
        person = pq.pop();
        add_person_to_scedule_list(person.personId, max(time, person.earliestTime));
        time = max(time, person.earliestTime) + competition_time;
        if(person.numberOfCompete > 1)
            pq.push(Tuple(person.Id,person.numberOfCompete - 1,time + breaktime)));
    }
    return pq.isEmpty();
}

The main problem:

After solving the trivial case we are ready to solve the original problem. In this case there are G = {g1, g2, ..., gm} grounds and we want to schedule P = {p1, p2, ..., pn} competitors. Like the trivial case we define a isItPossible(breaktime) function. Again we can prove that this function in monotonic, so we use binary search for finding the maximum value (like the above code). After that we only need to implement the isItPossible(breaktime) method. In this case implementing this method is little tricky.

For this method you can do some heuristic algorithms or some creative greedy ones (for example distribute each person start time base on breakTime over all grounds and check whether it is possible to do it for all persons or not). But again I suggest you to use Greedy algorithm and priority queue like the trivial case. Your tuple should also contains number of times that person compete in each ground, and when you want to increase time and sweep it, you should iterate over all grounds and schedule them simultaneously.

Hope it can help you. Of course there are some evolutionary algorithms like genetic or PSO to solve it (I can also explain them if you want) But using the above method is much simpler to implement and debug.

Upvotes: 4

Felipe Valdes
Felipe Valdes

Reputation: 2217

What an interesting problem!

Here is how I'd tackle it:

  1. Set up a random schedule which works (but doesn't fit the criteria).
  2. Write a function that can swap 2 performances
  3. Write a shuffler which uses swap() many times in order to get a new timetable
  4. Write a function to calculate a score(), how good is this particular schedule? does it have a lot of breaks between performances?

    • A score should sum all the performance gaps together, this is the function we want to maximise.
  5. Write an algorithm that takes a "search" approach and backtracking to the problem, and let it run for a couple hours, the backtracking should:

    • Swap stuff
    • See if the swapped stuff has a better score
    • if so, continue from swapped
    • otherwise, backtrack

It can take a while, but the program can generate a better timetable.

Let us know if this approach helps.

Upvotes: 2

Related Questions