1

I'm trying to write an algorithm that takes a list of events (in this case, screenings of movies - each movie usually has more than one screening, although that's not always the case) and returns the best possible combination of events, i.e. the one that sacrifices the least amount of movies.

Movie schedule

I came up with this algorithm which I applied manually, but I'm trying to turn it into code and having some trouble. What I did (manually) was go screening by screening, checking whether it had conflicts with any other screening. If it did, it would resolve the conflicts (let's say it's a conflict between the first screenings of Movie A and Movie B) by choosing one of the two conflicting movies* (Movie A), deleting all other screenings for Movie A, and deleting that screening of Movie B. After doing that, it would look for another screening of Movie B, check whether it had any conflicts, and repeat the process.

* Usually the one I was currently checking, but some criteria could affect the decision, e.g. if one of the two conflicting movies had a free (conflict-less) screening somewhere else, I would just choose that one and resolve the conflict that way. Conversely, if one of the two movies had no other screenings, I would choose that one.

Eventually this "branch" would end up arriving at a free (conflict-less) screening, which would mean the entire branch was solved (i.e. all of the movies involved could be watched without sacrificing any others), or it would arrive at a stalemate, which would mean that this combination of decisions did not lead to a perfect solution. In this case, one would go back up a level of recursion (i.e. go back one decision) and try choosing the other screening. If that didn't work, go back up another level and try again. If there was no combination of decisions in that branch that led to a solved branch, a decision had to be made by the human to sacrifice one of the movies that was causing the issue.

That's pretty much what I'm trying to code, and I wrote an attempt at it in a very stream-of-consciousness way just trying to get it out quickly cause I was excited, not thinking far ahead and just fixing issues as they came up. It worked for the movie festival I was trying it on, which was somewhat small (no more than 4 screenings at the same time), but crashed when I tried it on a larger one (6+ screenings at the same time sometimes). It exceeds the maximum stack size, probably because of a problem in the code although I'm not sure if it might just be running correctly and just getting into those levels of recursion. On this note, not sure whether this would be better solved with a while loop instead of recursively.

Yesterday I decided to scrap the algorithm I wrote and start from scratch in a better planned manner, and found the main 2 issues I see looking into the future are:

  • The ability for the algorithm to "go back" and switch its previous decision when a branch failed. This is a core mechanic of the algorithm but I'm struggling to think of a good way to code it. In my previous one I implemented a "checkpoint" system where, if a branch failed, it would revert back to the last checkpoint and continue on from there, and if it succeeded, it would mark that one as the new checkpoint. But that only worked top-level, and not for "internal" decisions.
  • The fact that most schedules will have more than one solution. If the solution is perfect (i.e. no movies sacrificed), then the issue is very minor in that either way you won't be missing any movies, and at most you'll be deprived of choosing between a few screenings of the same movies. But if there is no perfect solution, now the choice might be between having to decide between 2 movies that you really wanna watch, and between 2 movies that you don't care about that much. This means that the algorithm would need to go over every single possible combination of screenings to find the best possible one(s), and if there's no perfect solution it would show you the different possibilities. But this would be brute forcing the problem which (unless strictly necessary) is not really what I'm looking for.

I'd appreciate any help or guidance in the matter. For reference, I'm writing this in Typescript + React + Next.js, but I think this is a more general CS question so again, any help is appreciated.

The current algorithm as well as mock data can be found here: https://jsfiddle.net/procz/82n3jxLb/

const findBestSchedule = (
  screeningData: IScreening[],
  movies: IMovie[]
): IScreening[] => {
  const debugLog = (type: string, args) => {
    if (!DEBUG_LOG) return;
    if (type === "process") {
      return console.log(
        `processing ${findMovie(
          movies,
          args.screening
        ).name.toUpperCase()} ${args.screening.date.getDate()} ${args.screening.date.getHours()}hs - called from ${
          args.calledFrom
        }, currentArray:`,
        args.screenings
      );
    }
    if (type === "conflict") {
      return console.log(
        `conflict between ${findMovie(
          args.movies,
          args.screening1
        ).name.toUpperCase()} ${args.screening1.date.getDate()} ${args.screening1.date.getHours()}hs and ${findMovie(
          movies,
          args.screening2
        ).name.toUpperCase()} ${args.screening2.date.getDate()} ${args.screening2.date.getHours()}hs`
      );
    }

    if (type === "free") {
      return console.log(
        `${findMovie(
          args.movies,
          args.screening
        ).name.toUpperCase()} ${args.screening.date.getDate()} ${args.screening.date.getHours()}hs is free, deleting all other ${findMovie(
          args.movies,
          args.screening
        ).name.toUpperCase()}`
      );
    }

    if (type === "recursion") {
      return console.log(args.recursionLevel, args.array);
    }

    if (type === "unsolvable") {
      return console.log(
        `unsolvable conflict between ${findMovie(
          args.movies,
          args.screening1
        ).name.toUpperCase()} and ${findMovie(
          args.movies,
          args.screening2
        ).name.toUpperCase()}, backtracking`
      );
    }

    if (type === "decision") {
      return console.log(
        `recursion level: ${args.recursionLevel} - choosing ${findMovie(
          args.movies,
          args.screening1
        ).name.toUpperCase()} ${args.screening1.date.getDate()} ${args.screening1.date.getHours()}hs, deleting ${findMovie(
          args.movies,
          args.screening2
        ).name.toUpperCase()} ${args.screening2.date.getDate()} ${args.screening2.date.getHours()}hs, deleting all other ${findMovie(
          args.movies,
          args.screening1
        ).name.toUpperCase()}`
      );
    }
    if (type === "noConflict") {
      return console.log(
        `no conflicts found for ${findMovie(
          args.movies,
          args.screening
        ).name.toUpperCase()} ${args.screening.date.getDate()} ${args.screening.date.getHours()}hs, deleting all other ${findMovie(
          args.movies,
          args.screening
        ).name.toUpperCase()}`
      );
    }
  };
  const sortedScreenings = [...screeningData].sort(
    (a, b) => b.date.getTime() - a.date.getTime()
  );

  let latestScreeningArray = sortedScreenings;
  let recursionLevel = 0;
  let lastSafeArray = latestScreeningArray;
  let lastSafeRecursionLevel = recursionLevel;

  const processScreening = (
    screenings: IScreening[],
    screening: IScreening,
    calledFrom ? : string
  ): boolean => {
    debugLog("process", {
      screening,
      calledFrom,
      screenings
    });
    const findConflictingScreenings = (
      screenings: IScreening[],
      screening: IScreening
    ): IScreening[] => {
      if (!screenings) return [];
      return [...screenings].filter(
        (otherScreening) =>
        screening.id !== otherScreening ? .id &&
        screeningsOverlap(screening, otherScreening, movies)
      );
    };

    const conflictingScreenings = findConflictingScreenings(
      screenings,
      screening
    );

    if (conflictingScreenings.length) {
      const resolveConflict = (
        screening: IScreening,
        otherScreening: IScreening
      ): boolean => {
        debugLog("conflict", {
          screening1: screening,
          screening2: otherScreening,
          movies,
        });

        const findNextScreenings = (
          screenings: IScreening[],
          screening: IScreening
        ): IScreening[] => {
          return [...screenings]
            .reverse()
            .filter(
              (currentScreening) =>
              currentScreening.id !== screening.id &&
              findMovie(movies, currentScreening).id ===
              findMovie(movies, screening).id
            );
        };

        const findFreeScreening = (
          screenings: IScreening[],
          screening: IScreening
        ): IScreening => {
          return findNextScreenings(screenings, screening).find(
            (nextScreening) =>
            !findConflictingScreenings(screenings, nextScreening).length
          );
        };

        const freeOtherScreening = findFreeScreening(
          latestScreeningArray,
          otherScreening
        );
        const freeScreening = findFreeScreening(
          latestScreeningArray,
          screening
        );

        if (freeOtherScreening || freeScreening) {
          /* FREE SCREENING */
          debugLog("free", {
            screening: freeOtherScreening || freeScreening,
            movies,
          });
          latestScreeningArray = deleteAllOtherScreenings(
            latestScreeningArray,
            freeOtherScreening || freeScreening,
            movies
          );
          return true;
        } else {
          /* NO FREE SCREENINGS */

          const decideAndMoveToNextStep = (
            screenings: IScreening[],
            screening1: IScreening,
            screening2: IScreening
          ): boolean => {
            latestScreeningArray = removeScreening(
              deleteAllOtherScreenings(screenings, screening1, movies),
              screening2
            );
            debugLog("recursion", {
              recursionLevel,
              array: latestScreeningArray,
            });
            recursionLevel++;
            const nextStepOfBranchIsSolvable = processScreening(
              latestScreeningArray,
              findNextScreenings(screenings, screening2)[0],
              `decideAndMoveToNextStep`
            );
            if (!nextStepOfBranchIsSolvable) {
              debugLog("unsolvable", {
                movies,
                screening1,
                screening2
              });
              latestScreeningArray = screenings;
            }
            return nextStepOfBranchIsSolvable;
          };

          const nextScreeningOfOther = findNextScreenings(
            latestScreeningArray,
            otherScreening
          )[0];

          if (nextScreeningOfOther) {
            debugLog("decision", {
              recursionLevel,
              movies,
              screening1: screening,
              screening2: otherScreening,
            });
            return decideAndMoveToNextStep(
              latestScreeningArray,
              screening,
              otherScreening
            );
          } else {
            const nextScreening = findNextScreenings(
              latestScreeningArray,
              screening
            )[0];
            if (nextScreening) {
              debugLog("decision", {
                recursionLevel,
                movies,
                screening1: otherScreening,
                screening2: screening,
              });
              return decideAndMoveToNextStep(
                latestScreeningArray,
                otherScreening,
                screening
              );
            } else {
              debugLog("unsolvable", {
                movies,
                screening1: screening,
                screening2: otherScreening,
              });
              return false;
            }
          }
        }
      };

      for (let i = conflictingScreenings.length - 1; i >= 0; i--) {
        const branchResolved = resolveConflict(
          screening,
          [...conflictingScreenings].reverse()[i]
        );
        if (!branchResolved) {
          latestScreeningArray = lastSafeArray;
          break;
        }
      }
      debugLog("noConflicts", {
        movies,
        screening
      });
      debugLog("recursion", {
        recursionLevel,
        array: latestScreeningArray
      });
      if (recursionLevel > 0) {
        recursionLevel--;
      }
      return true;
    } else {
      const updatedScreenings = deleteAllOtherScreenings(
        screenings,
        screening,
        movies
      );
      latestScreeningArray = updatedScreenings;
      if (recursionLevel > 0) {
        recursionLevel--;
      }
      return false;
    }
  };

  for (let i = latestScreeningArray.length - 1; i >= 0; i--) {
    if (!!latestScreeningArray[i]) {
      lastSafeArray = latestScreeningArray;
      lastSafeRecursionLevel = recursionLevel;
      processScreening(
        latestScreeningArray,
        latestScreeningArray[i],
        "overarching"
      );
    }
  }
  return latestScreeningArray;
};
Nicolas
  • 87
  • 2
  • 8
  • Could you be more explicit at what the constraints are that must be satisfied? In the image I see dates, timeslots, vertical lanes, colors, ... How do these all play a role in constraints? Is there a maximum number of days? Is there a concept of valid time periods? ...etc. Is the goal to schedule all screenings? How come the screenings already have a timestamp in the *input*? Isn't that the item that needs to be determined? What is the role of location? ...etc. – trincot May 16 '22 at 10:14
  • @trincot The movies and screenings are decided by a film festival, the user loads them up here and the goal is for the algorithm to find the best combination of screenings to watch as many of the movies as possible. Colors, lanes, etc. don't matter, they're just a visualization aid for the user. Only things that matter (as far as the algorithm is concerned) are the movies (mostly for their runtime - to know when each screening is gonna end) and the screenings themselves. – Nicolas May 16 '22 at 12:14
  • *"as many movies as possible"*: what is the contraint that would prohibit to watch them all? Is there a time limit? Are there daily timeslots, a limited number of days? If so, how are these constraints provided? – trincot May 16 '22 at 12:19
  • To give a very simplified example, if there's a Movie A and a Movie B, both with only one screening each, and both screenings are at the same time, you cannot possibly watch both Movie A and Movie B. The constraint is that there's only a limited number of screenings for each movie (usually between 1 and 3 screenings), and there probably will be situations like the one I described above, perhaps a bit more complex (e.g. A1 (first screening of Movie A) clashes with B1, B2 clashes with C1, C2 clashes with A1 - you can only watch 2 of the 3 movies). – Nicolas May 16 '22 at 14:11
  • There are no daily screenings, movies have a limited number of screenings throughout the festival, which lasts a limited number of days. The constraints (movies and their respective screenings' dates and times) would be provided by the festival and inputted by the user to the website. – Nicolas May 16 '22 at 14:13
  • Oh so this is not about planning the screenings? They already have been planned? The question is to which screenings I should go so to see the most different movies? Is location a factor to use? – trincot May 16 '22 at 14:46
  • I feel as though there's an input missing: the user-specific ranking of desirability. Even though the movies may be unknown, there's sure to be meta-information about their appeals: "Oh, I *have to* hit the latest Sylvia Caminer film! But I don't care about Joe Shmoe's latest." And without something like this, unless we throw in some randomization, it would simply give everyone the same schedule, likely leading to everyone trying to cram into the same showings. – Scott Sauyet May 16 '22 at 14:48
  • @trincot That is correct. Location could possibly be a factor in the long term but right now it doesn't matter, I'm content with just being able to work it out purely based on time. – Nicolas May 16 '22 at 15:02
  • 1
    @ScottSauyet Right, I thought about adding something like that at some point, mostly for a better user experience. As for your latter point of giving everyone the same schedule, the idea would be that each person adds the movies that they wanna watch and not all the movies in the festival. A desirability ranking could possibly make it a bit better but the rankings would probably end up being very similar between users (the most popular movies being at the top) and thus the schedules being similar as well. If adding it would make the algorithm easier to code, I'm all for it, though! – Nicolas May 16 '22 at 15:03
  • Ok, a list of desired movies serves a similar purpose. So you're not trying to schedule *all* movies but the largest possible subset of the chosen ones. It should be doable. It would be great to have a smallish example or two in the question. This is interesting, and is probably a well-known problem to computer scientists, but I don't know that for sure, or what name to look for if it is. If I find some time soon, I'll try to think about it. – Scott Sauyet May 16 '22 at 16:10
  • @ScottSauyet I included two examples in the JSFiddle, one that works with the algorithm that I wrote, the other that runs into a stack overflow. Could be fixed by using a loop instead of recursion, but I haven't tried it, mostly because that code is already fixes upon fixes so I wanted to write it from scratch. – Nicolas May 16 '22 at 17:16
  • I am in a corporate environment that blocks JSFiddle for some unknown reason. Some of the others alternatives are still open, but the usual idea is to include enough that the question stands by itself, and external sites are just used for supporting detail. – Scott Sauyet May 16 '22 at 17:35
  • I'll add them to the question, gimme a sec – Nicolas May 16 '22 at 17:46
  • It doesn't let me add the extra code to the post since it exceeds the character limit (by quite a bit). If you send one of the alternatives that are available to you I can paste the code there instead! – Nicolas May 16 '22 at 17:50
  • 1
    I won't be able to work on it during working hours today and I won't be blocked at home, so it's not a big deal. But as a general rule, try to post a [mcve] so that others can work on it even if JSFiddle goes offline. – Scott Sauyet May 16 '22 at 19:10

1 Answers1

2

I would suggest mixed integer programming. There’s a library javascript-lp-solver available on NPM, which is probably OK for light use like this.

The basic idea is to have a 0–1 variable for each movie indicating whether we see any screening of that movie, and a 0–1 variable for each screening indicating whether we see that screening. We want to maximize the number of movies seen (or maybe some more complicated linear function).

There are two kinds of constraints. For each time, we cannot attend more than one screening during that time (for each time, the sum of the screening variables at that time is less than or equal to 1). This is handled in the code below with a sweep-line algorithm. For each movie, we can’t see it unless we attend one of its screenings (for each movie, sum of screening variables for that movie minus movie variable greater than or equal to 0).

I implemented this in JavaScript below (poorly, since I’m not fluent, and the API on this library is not the best).

const movies = [
  {
    id: 1,
    runtime: 121,
  },
  {
    id: 2,
    runtime: 116,
  },
  {
    id: 3,
    runtime: 144,
  },
  {
    id: 4,
    runtime: 96,
  },
  {
    id: 5,
    runtime: 108,
  },
  {
    id: 6,
    runtime: 117,
  },
  {
    id: 7,
    runtime: 92,
  },
  {
    id: 8,
    runtime: 110,
  },
  {
    id: 9,
    runtime: 90,
  },
  {
    id: 10,
    runtime: 99,
  },
  {
    id: 11,
    runtime: 62,
  },
  {
    id: 12,
    runtime: 116,
  },
  {
    id: 13,
    runtime: 85,
  },
  {
    id: 14,
    runtime: 92,
  },
  {
    id: 15,
    runtime: 85,
  },
  {
    id: 16,
    runtime: 90,
  },
  {
    id: 17,
    runtime: 103,
  },
  {
    id: 18,
    runtime: 113,
  },
  {
    id: 19,
    runtime: 72,
  },
  {
    id: 20,
    runtime: 109,
  },
  {
    id: 21,
    runtime: 109,
  },
  {
    id: 22,
    runtime: 142,
  },
  {
    id: 23,
    runtime: 96,
  },
  {
    id: 24,
    runtime: 73,
  },
  {
    id: 25,
    runtime: 86,
  },
  {
    id: 26,
    runtime: 106,
  },
  {
    id: 27,
    runtime: 97,
  },
  {
    id: 28,
    runtime: 100,
  },
  {
    id: 29,
    runtime: 146,
  },
  {
    id: 30,
    runtime: 146,
  },
  {
    id: 31,
    runtime: 146,
  },
  {
    id: 32,
    runtime: 108,
  },
  {
    id: 33,
    runtime: 89,
  },
  {
    id: 34,
    runtime: 126,
  },
  {
    id: 35,
    runtime: 121,
  },
  {
    id: 36,
    runtime: 150,
  },
  {
    id: 37,
    runtime: 108,
  },
  {
    id: 38,
    runtime: 103,
  },
  {
    id: 39,
    runtime: 130,
  },
  {
    id: 40,
    runtime: 83,
  },
  {
    id: 41,
    runtime: 136,
  },
  {
    id: 42,
    runtime: 85,
  },
  {
    id: 43,
    runtime: 179,
  },
  {
    id: 44,
    runtime: 106,
  },
  {
    id: 45,
    runtime: 107,
  },
  {
    id: 46,
    runtime: 93,
  },
  {
    id: 47,
    runtime: 75,
  },
  {
    id: 48,
    runtime: 86,
  },
  {
    id: 49,
    runtime: 80,
  },
  {
    id: 50,
    runtime: 80,
  },
];

const screenings = [
  {
    id: 1,
    date: "2021-11-18T23:15:00.000Z",
    movieId: 18,
  },
  {
    id: 2,
    date: "2021-11-20T14:15:00.000Z",
    movieId: 19,
  },
  {
    id: 3,
    date: "2021-11-19T21:00:00.000Z",
    movieId: 19,
  },
  {
    id: 4,
    date: "2021-11-19T15:03:00.000Z",
    movieId: 19,
  },
  {
    id: 5,
    date: "2021-11-19T23:04:00.000Z",
    movieId: 20,
  },
  {
    id: 6,
    date: "2021-11-19T17:00:00.000Z",
    movieId: 20,
  },
  {
    id: 7,
    date: "2021-11-25T20:00:00.000Z",
    movieId: 20,
  },
  {
    id: 8,
    date: "2021-11-19T14:00:00.000Z",
    movieId: 21,
  },
  {
    id: 9,
    date: "2021-11-19T20:00:00.000Z",
    movieId: 21,
  },
  {
    id: 10,
    date: "2021-11-25T17:00:00.000Z",
    movieId: 21,
  },
  {
    id: 11,
    date: "2021-11-19T21:00:00.000Z",
    movieId: 22,
  },
  {
    id: 12,
    date: "2021-11-20T18:00:00.000Z",
    movieId: 22,
  },
  {
    id: 13,
    date: "2021-11-19T16:00:00.000Z",
    movieId: 23,
  },
  {
    id: 14,
    date: "2021-11-19T14:30:00.000Z",
    movieId: 24,
  },
  {
    id: 15,
    date: "2021-11-19T23:30:00.000Z",
    movieId: 24,
  },
  {
    id: 16,
    date: "2021-11-20T19:30:00.000Z",
    movieId: 24,
  },
  {
    id: 17,
    date: "2021-11-20T14:30:00.000Z",
    movieId: 25,
  },
  {
    id: 18,
    date: "2021-11-20T20:30:00.000Z",
    movieId: 25,
  },
  {
    id: 19,
    date: "2021-11-21T22:30:00.000Z",
    movieId: 25,
  },
  {
    id: 20,
    date: "2021-11-20T14:00:00.000Z",
    movieId: 26,
  },
  {
    id: 21,
    date: "2021-11-20T20:00:00.000Z",
    movieId: 26,
  },
  {
    id: 22,
    date: "2021-11-20T15:00:00.000Z",
    movieId: 27,
  },
  {
    id: 23,
    date: "2021-11-20T21:00:00.000Z",
    movieId: 27,
  },
  {
    id: 24,
    date: "2021-11-21T14:15:00.000Z",
    movieId: 27,
  },
  {
    id: 25,
    date: "2021-11-20T18:00:00.000Z",
    movieId: 28,
  },
  {
    id: 26,
    date: "2021-11-21T00:00:00.000Z",
    movieId: 28,
  },
  {
    id: 27,
    date: "2021-11-21T17:15:00.000Z",
    movieId: 28,
  },
  {
    id: 28,
    date: "2021-11-21T21:00:00.000Z",
    movieId: 31,
  },
  {
    id: 29,
    date: "2021-11-20T21:00:00.000Z",
    movieId: 31,
  },
  {
    id: 30,
    date: "2021-11-21T00:00:00.000Z",
    movieId: 32,
  },
  {
    id: 31,
    date: "2021-11-29T00:00:00.000Z",
    movieId: 32,
  },
  {
    id: 32,
    date: "2021-11-23T18:30:00.000Z",
    movieId: 32,
  },
  {
    id: 33,
    date: "2021-11-21T14:00:00.000Z",
    movieId: 33,
  },
  {
    id: 34,
    date: "2021-11-21T20:00:00.000Z",
    movieId: 33,
  },
  {
    id: 35,
    date: "2021-11-22T22:30:00.000Z",
    movieId: 33,
  },
  {
    id: 36,
    date: "2021-11-21T18:00:00.000Z",
    movieId: 34,
  },
  {
    id: 37,
    date: "2021-11-22T23:15:00.000Z",
    movieId: 34,
  },
  {
    id: 38,
    date: "2021-11-21T20:15:00.000Z",
    movieId: 35,
  },
  {
    id: 39,
    date: "2021-11-27T21:00:00.000Z",
    movieId: 35,
  },
  {
    id: 40,
    date: "2021-11-22T15:00:00.000Z",
    movieId: 36,
  },
  {
    id: 41,
    date: "2021-11-23T00:00:00.000Z",
    movieId: 36,
  },
  {
    id: 42,
    date: "2021-11-23T14:15:00.000Z",
    movieId: 36,
  },
  {
    id: 43,
    date: "2021-11-22T13:45:00.000Z",
    movieId: 37,
  },
  {
    id: 44,
    date: "2021-11-22T18:30:00.000Z",
    movieId: 37,
  },
  {
    id: 45,
    date: "2021-11-22T21:30:00.000Z",
    movieId: 37,
  },
  {
    id: 46,
    date: "2021-11-22T17:30:00.000Z",
    movieId: 38,
  },
  {
    id: 47,
    date: "2021-11-22T23:30:00.000Z",
    movieId: 38,
  },
  {
    id: 48,
    date: "2021-11-23T16:30:00.000Z",
    movieId: 38,
  },
  {
    id: 49,
    date: "2021-11-22T21:00:00.000Z",
    movieId: 39,
  },
  {
    id: 50,
    date: "2021-11-28T18:00:00.000Z",
    movieId: 39,
  },
  {
    id: 51,
    date: "2021-11-23T18:00:00.000Z",
    movieId: 40,
  },
  {
    id: 52,
    date: "2021-11-27T21:30:00.000Z",
    movieId: 40,
  },
  {
    id: 53,
    date: "2021-11-24T18:00:00.000Z",
    movieId: 41,
  },
  {
    id: 54,
    date: "2021-11-23T18:00:00.000Z",
    movieId: 41,
  },
  {
    id: 55,
    date: "2021-11-25T18:00:00.000Z",
    movieId: 41,
  },
  {
    id: 58,
    date: "2021-11-23T21:00:00.000Z",
    movieId: 43,
  },
  {
    id: 59,
    date: "2021-11-24T21:00:00.000Z",
    movieId: 43,
  },
  {
    id: 60,
    date: "2021-11-24T14:30:00.000Z",
    movieId: 44,
  },
  {
    id: 61,
    date: "2021-11-24T20:30:00.000Z",
    movieId: 44,
  },
  {
    id: 62,
    date: "2021-11-25T16:30:00.000Z",
    movieId: 44,
  },
  {
    id: 63,
    date: "2021-11-25T21:00:00.000Z",
    movieId: 45,
  },
  {
    id: 64,
    date: "2021-11-26T15:00:00.000Z",
    movieId: 45,
  },
  {
    id: 70,
    date: "2021-11-24T15:00:00.000Z",
    movieId: 48,
  },
  {
    id: 71,
    date: "2021-11-24T21:00:00.000Z",
    movieId: 48,
  },
  {
    id: 72,
    date: "2021-11-26T00:00:00.000Z",
    movieId: 48,
  },
  {
    id: 76,
    date: "2021-11-24T14:00:00.000Z",
    movieId: 50,
  },
  {
    id: 77,
    date: "2021-11-24T20:00:00.000Z",
    movieId: 50,
  },
  {
    id: 78,
    date: "2021-11-27T17:00:00.000Z",
    movieId: 50,
  },
];

let model = {
  optimize: "movieSeen",
  opType: "max",
  constraints: {},
  variables: {},
  ints: {},
};
let constraintId = 1000;
for (const movie of movies) {
  ++constraintId;
  model.constraints[constraintId] = { max: 1 };
  model.variables["m" + movie.id] = { movieSeen: 1 };
  model.variables["m" + movie.id][constraintId] = 1;
  ++constraintId;
  model.constraints["sm" + movie.id] = { min: 0 };
  model.variables["m" + movie.id]["sm" + movie.id] = -1;
}
for (const screening of screenings) {
  ++constraintId;
  model.constraints[constraintId] = { min: 0 };
  model.variables["s" + screening.id] = { constraintId: 1 };
  model.variables["s" + screening.id]["sm" + screening.movieId] = 1;
  model.ints["s" + screening.id] = 1;
}

let runtimes = {};
for (const movie of movies) {
  runtimes[movie.id] = movie.runtime;
}
let events = [];
for (const screening of screenings) {
  const start = Date.parse(screening.date);
  events.push({ date: start, isStart: true, id: screening.id });
  const finish = start + runtimes[screening.movieId] * 60 * 1000;
  events.push({ date: finish, isStart: false, id: screening.id });
}
events.sort(function (a, b) {
  return a.date - b.date;
});

let previousIsStart = false;
let nowPlaying = {};
for (const event of events) {
  if (event.isStart) {
    nowPlaying[event.id] = true;
    previousIsStart = true;
  } else {
    if (previousIsStart) {
      ++constraintId;
      model.constraints[constraintId] = { max: 1 };
      for (const id of Object.keys(nowPlaying)) {
        model.variables["s" + id][constraintId] = 1;
      }
    }
    delete nowPlaying[event.id];
    previousIsStart = false;
  }
}

let solver = require("javascript-lp-solver");
let results = solver.Solve(model);
console.log(results);

Output:

{
  feasible: true,
  result: 27,
  bounded: true,
  isIntegral: true,
  s1: 1,
  s64: 1,
  s12: 1,
  s15: 1,
  s42: 1,
  s20: 1,
  s24: 1,
  s29: 1,
  s31: 1,
  s37: 1,
  s19: 1,
  s46: 1,
  s51: 1,
  s53: 1,
  s58: 1,
  s71: 1,
  s13: 1,
  s3: 1,
  s8: 1,
  s34: 1,
  s7: 1,
  s26: 1,
  s39: 1,
  s43: 1,
  s49: 1,
  s76: 1,
  s62: 1,
  m18: 1,
  m19: 1,
  m20: 1,
  m21: 1,
  m22: 1,
  m23: 1,
  m24: 1,
  m25: 1,
  m26: 1,
  m27: 1,
  m28: 1,
  m31: 1,
  m32: 1,
  m33: 1,
  m34: 1,
  m35: 1,
  m36: 1,
  m37: 1,
  m38: 1,
  m39: 1,
  m40: 1,
  m41: 1,
  m43: 1,
  m44: 1,
  m45: 1,
  m48: 1,
  m50: 1
}
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • This is crazy. So this is the stuff I missed on by dropping out of that CS degree, huh? Heh. I can't thank you enough, this works perfectly even with your so-called "poor" code. My second question still stands, though, and you might be able to shine some light on it (obviously if you feel like it). For instance, if I give it a schedule where it must sacrifice one movie, couldn't there possibly be other solutions which also sacrifice only one (other) movie? In that case, how could one go about leaving that decision in the user's hands? Perhaps adding "desirability scores" to each movie? – Nicolas May 17 '22 at 01:08
  • 1
    @Nicolas Sadly you probably *wouldn't* have learned this as a CS major, and it's a damn shame because these tools are mathematically grounded and so useful in practice. Re your second question, yes, you could change `movieSeen: 1` to `movieSeen: movie.desirabilityScore`. Another possibility would be to find a MIP code that returns all optimal solutions, but that might be tricky to scale. – David Eisenstat May 17 '22 at 02:50
  • Eisentat Right, I had to add desirability scores anyway for a different feature of the site so I might as well go ahead with that option and experiment with the values to see what comes out. Thanks again! – Nicolas May 17 '22 at 12:10