0

I am trying to create a schedule function that uses only python built-in modules that will return the maximum number of non overlapping appointments. The function's input is a list of lists, the inner list contains 2 integer elements, the start and end time. The start and end times cannot be changed and if the start time of one meeting is the same as the end time of another, they are not considered overlapping. For example:

Input:

meetings = [[0, 1], [1, 2], [2, 3], [3, 5], [4, 5]]
max_meetings(meetings)

Output:

4

The Code I have now just brute forces it and is wildly inefficient in both memory and execution time. Even though it's fun to use classes, it seems like there would be a better way to do it.

def max_meetings(meetings):
    '''
    Return the maximum number of non overlapping meeting that I can attend

    input:
        meetings - A list of lists. the inner list contains 2 values, the start
            time[0] and the end time[1].

    returns:
        total - The total number of non overlapping meetings that I can attend.
    '''

    num_meetings = len(meetings)
    assert (num_meetings <= 100)

    appt_obj = [Appt(o) for o in meetings]

    total = 0
    for appt in appt_obj:
        schedule = Schedule()
        schedule.add_meeting(appt)
        counter = 0
        for comp_appt in appt_obj:
            counter += 1
            schedule.add_meeting(comp_appt)
            # If there isnt a chance, break to save some time
            if ((num_meetings - counter) < (total - schedule.meetings)):
                break

        if schedule.meetings > total:
            total = schedule.meetings

    return total


class Schedule:
    '''
    A class to hold my entire schedule. Can add
    appointments
    '''
    def __init__(self):
        self.times = set()
        self.meetings = 0

    def add_meeting(self, appt):
        points = range(appt.start, appt.end)
        if any(x in self.times for x in points):
            pass
        else:
            # This for loop also seems unnecessary
            for p in points:
                self.times.add(p)
            self.meetings += 1


class Appt:
    '''
    A class for an appointment
    '''

    def __init__(self, meeting):
        assert (meeting[0] >= 0)
        assert (meeting[1] <= 1000000)
        self.start = meeting[0]
        self.end = meeting[1]
veda905
  • 782
  • 2
  • 12
  • 32

2 Answers2

1

Classes are often slightly slower-running but easier to understand; the problem in your code is not the classes, it's a poor algorithm (you could write a super-optimized machine-code bubblesort and it would still be slow).

This problem is ideally suited to dynamic programming:

  • sort all of your meetings in ascending order by end time

  • maintain a list of end-times like lst[n] = t where n is the number of meetings and t is the earliest end-time by which that is possible. Let lst[0] = float("-inf") as a no-meeting placeholder.

  • to insert a meeting, find the lowest n such that lst[n] <= start, then if lst[n + 1] does not exist or is greater than end let lst[n + 1] = end.

  • when you are finished, the maximum number of meetings is len(lst) - 1.


Based on your example,

meetings = [[0, 1], [1, 2], [2, 3], [3, 5], [4, 5]]

you should end up with

lst = [-inf, 1, 2, 3, 5]

which should be read as "it is possible to have at most 1 meeting ending by 1, at most 2 meetings ending by 2, at most 3 meetings ending by 3, and at most 4 meetings ending by 5".

Note that this does not tell you which combination of meetings give that result, or how many such combinations are possible - only that at least one such combination exists.


Edit: try the following:

from bisect import bisect

class Meeting:
    # save about 500 bytes of memory
    #  by not having a __dict__
    __slots__ = ("start", "end")

    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __lt__(self, other):
        return self.end < other.end

def max_meetings(meetings):
    meetings.sort()    # Meeting.__lt__ provides a natural sort order by meeting.end

    # -1 is just a "lower than any actual time" gatepost value
    lst = [-1]

    for m in meetings:
        # find the longest chain of meetings which this meeting can follow
        i = bisect(lst, m.start)

        if i == len(lst):
            # new most-meetings value
            lst.append(m.end)
        elif m.end < lst[i]:
            # new earliest finish-time for 
            lst[i] = m.end

    # what is the most meetings we can attend?
    # print(lst)
    return len(lst) - 1

which runs like

meetings = [
    Meeting(0, 1),
    Meeting(1, 2),
    Meeting(2, 3),
    Meeting(3, 5),
    Meeting(4, 5)
]

print(max_meetings(meetings))   # => 4
Hugh Bothwell
  • 55,315
  • 8
  • 84
  • 99
  • For point 2, the list of end times `lst` is a list of the start times that is sorted by the end time. Then for point 3, within a loop look at all of the sorted meetings and compare that start to the the `lst` list? wouldn't that come out to always be equal?I guess the problem I am having is the definition of the lst... – veda905 Feb 13 '16 at 17:00
  • Thank you for the edit, the code made it much clearer what you meant. Your solution works perfectly and runs orders of magnitude faster then my solution. Yours: **5.09 µs** compared to mine: **474 ms** – veda905 Feb 13 '16 at 19:08
  • Hi, Can you explain this with a dry-run? Like how did you end up with `lst = [-inf, 1, 2, 3, 5]` – Mani Kumar Reddy Kancharla Feb 08 '19 at 10:30
0

How about removing an item from the original list in iterations and then only check against those left:

def overlaps(one, other):
    if one[0] <= other[0] and other[1] <= one[1]:
        return True
    if other[0] <= one[0] and one[1] <= other[1]:
        return True
    else:
        return False

def max_meetings(slots):
    unique = 0
    while len(slots) > 0:
        meeting = slots.pop(0)
        for slot in slots:
            if overlaps(meeting, slot):
                break
        else:
            unique +=1
    return unique

meetings = [[0, 1], [1, 2], [2, 3], [3, 5], [4, 5]]
print(max_meetings(meetings))

If there are edge-cases that overlaps didn't cover, then you can extend the logic with ease because it is neatly decoupled in seperate functions.

root-11
  • 1,727
  • 1
  • 19
  • 33
  • This seems to work for most cases but when trying to time it I get `>>> %timeit max_meetings(meetings) The slowest run took 194.35 times longer than the fastest. This could mean that an intermediate result is being cached 10000000 loops, best of 3: 133 ns per loop`. Not sure what that means. – veda905 Feb 13 '16 at 16:00