tests = [ { "data": { "lesson": [1594663200, 1594666800], "pupil": [ 1594663340, 1594663389, 1594663390, 1594663395, 1594663396, 1594666472, ], "tutor": [1594663290, 1594663430, 1594663443, 1594666473], }, "answer": 3117, }, { "data": { "lesson": [1594702800, 1594706400], "pupil": [ 1594702789, 1594704500, 1594702807, 1594704542, 1594704512, 1594704513, 1594704564, 1594705150, 1594704581, 1594704582, 1594704734, 1594705009, 1594705095, 1594705096, 1594705106, 1594706480, 1594705158, 1594705773, 1594705849, 1594706480, 1594706500, 1594706875, 1594706502, 1594706503, 1594706524, 1594706524, 1594706579, 1594706641, ], "tutor": [ 1594700035, 1594700364, 1594702749, 1594705148, 1594705149, 1594706463, ], }, "answer": 3577, }, { "data": { "lesson": [1594692000, 1594695600], "pupil": [1594692033, 1594696347], "tutor": [1594692017, 1594692066, 1594692068, 1594696341], }, "answer": 3565, }, ] def get_smaller_interval(interval_A, interval_B) -> list: """Если один из интервалов включает в себя другой, возвращаем меньший. Если интервалы не входят друг в дргуа, возвращаем пустой интервал.""" if interval_A[0] <= interval_B[0] and interval_A[1] >= interval_B[1]: return interval_B if interval_B[0] <= interval_A[0] and interval_B[1] >= interval_A[1]: return interval_A return [] def absorb_small_included_intervals(intervals: list) -> list: """Возвращает список интервалов без вложенных интервалов. Вложенный интервал - интервал меньшего размера, полностью входящий в больший. Стоит учитывать, что функция работает только если интервалы совпадают по началу или концу, если интервалы входят друг в друга частично -- мы их не обрабатываем. """ small_intervals = [ get_smaller_interval(interval, intervals[k]) for i, interval in enumerate(intervals) for k in range(i + 1, len(intervals)) if get_smaller_interval(interval, intervals[k]) ] result = [i for i in intervals if i not in small_intervals] return result def get_union_of_partly_intersecting_intervals(interval_A, interval_B) -> list: """Если интервалы А и Б пересекаются, то возвращаем новый интервал, включающий А и Б.""" # планирую поступать по тому же принципу что и выше: добавлять # сумму в исходный список, итого получим список с пересечениями, # который потом можно прогнать через поглатитель меньших # интервалов # если один из интервалов пустой, то пересечение будет пустым if not interval_A or not interval_B: return [] # если какой-то из инетрвалов имеет нулевую длину (начало=конец), # то пересечение пустое if interval_A[0] == interval_A[1] or interval_B[0] == interval_B[1]: return [] # нужно проверить, что интервалы вообще пересекаются # А и B не пересекаются, если # начало А > конец B, или если начало B > конец А if interval_A[0] > interval_B[1] or interval_B[0] > interval_A[1]: return [] # сумма интервалов: наименьшее начало из А и Б, и наибольший конец # из А и Б interval_begin = min(interval_A[0], interval_B[0]) interval_end = max(interval_A[1], interval_B[1]) return [interval_begin, interval_end] def get_extended_intervals(intervals) -> list: """Возвращает расширенные интервалы из списка интервалов.""" extended_intervals = [ get_union_of_partly_intersecting_intervals(interval, intervals[k]) for i, interval in enumerate(intervals) for k in range(i + 1, len(intervals)) if get_union_of_partly_intersecting_intervals(interval, intervals[k]) ] uniq_extended_intervals = [i for i in extended_intervals if i not in intervals] return uniq_extended_intervals def get_intersection(interval_A, interval_B) -> list: # если один из интервалов пустой, то пересечение будет пустым if not interval_A or not interval_B: return [] # если какой-то из инетрвалов имеет нулевую длину (начало=конец), # то пересечение пустое if interval_A[0] == interval_A[1] or interval_B[0] == interval_B[1]: return [] # нужно проверить, что интервалы вообще пересекаются # А и B не пересекаются, если # начало А > конец B, или если начало B > конец А if interval_A[0] > interval_B[1] or interval_B[0] > interval_A[1]: return [] # начало интервала = наименьший из начал интервалов А и B # конец интервала это наименьший из концов интервалов А и B interval_begin = max(interval_A[0], interval_B[0]) interval_end = min(interval_A[1], interval_B[1]) return [interval_begin, interval_end] def normilize_intervals(intervals) -> list: """Разбивает полученный список на пары, расширяет интервалы, убирает вложенные интервалы.""" # из списка [1,2,3,4] формируем [[1,2], [3,4]] intervals_ = [intervals[i : i + 2] for i in range(0, len(intervals), 2)] # получаем расширенные интервалы extended_intervals = get_extended_intervals(intervals_) # убираем вложенные инетрвалы n_intervals = absorb_small_included_intervals(intervals_ + extended_intervals) return n_intervals def appearance(intervals): lesson = intervals["lesson"] pupil = normilize_intervals(intervals["pupil"]) tutor = normilize_intervals(intervals["tutor"]) # получаем все интервалы студента на уроке pupil_on_lesson = [ get_intersection(lesson, p) for p in pupil if get_intersection(lesson, p) ] # по очереди сравниваем каждый из интервалов преподавателя с # каждым из интервалов пересечения студента на уроке tutor_on_pupil = [ get_intersection(pol, t) for t in tutor for pol in pupil_on_lesson ] # создаем список из продолжительности каждого интервала, если он # не пустой, а затем суммируем все элементы answer = sum([intr[1] - intr[0] for intr in tutor_on_pupil if intr]) return answer if __name__ == "__main__": for i, test in enumerate(tests): test_answer = appearance(test["data"]) assert ( test_answer == test["answer"] ), f'Error on test case {i}, got {test_answer}, expected {test["answer"]}'