Browse Source

Убрал квадратичную сложность

master
Дмитрий 3 years ago
parent
commit
fcaa4a6570
  1. 105
      intervals_cleaning.py
  2. 6
      readme.org
  3. 67
      task_3/intervals_cleaning.py
  4. 48
      task_3/intervals_intersection.py
  5. 36
      task_3/task_3.py
  6. 158
      task_3/task_3_tests.py

105
intervals_cleaning.py

@ -1,105 +0,0 @@
"""Модуль, содержащий функции для чистки интервалов.
Под 'чисткой' подразумевается:
- удаление меньших интервалов, полностью входящих в большие (поглощение)
- расширение пересекающихся интервалов (сумма интервалов)
"""
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])
]
# в качестве результа у нас будет новый список, состоящий из
# принятого списка интервалов `intervals`, за исключением входящих меньших
# интервалов `small_intervals`
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:
"""Если интервалы A и B пересекаются, то возвращаем новый
интервал, включающий A и B."""
# если один из интервалов пустой, то пересечение будет пустым
if not interval_A or not interval_B:
return []
# если какой-то из интервалов имеет нулевую длину (начало=конец),
# то пересечение пустое
if interval_A[0] == interval_A[1] or interval_B[0] == interval_B[1]:
return []
# нужно проверить, что интервалы вообще пересекаются
# A и B не пересекаются, если
# начало A > конец B, или если начало B > конец A
if interval_A[0] > interval_B[1] or interval_B[0] > interval_A[1]:
return []
# сумма интервалов: берем наименьшее начало из A и B, и наибольший конец
# из A и B
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])
]
# список extended_intervals может содержать дублирующиеся
# интервалы из списка intervals, поэтому мы возвращаем только
# отсутствующие в intervals значения
uniq_extended_intervals = [i for i in extended_intervals if i not in intervals]
return uniq_extended_intervals
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

6
readme.org

@ -104,13 +104,13 @@ tutor – интервалы присутствия учителя
Запустить решение можно командой:
#+begin_src python
python task_3.py
python task_3/task_3.py
#+end_src
Если ничего не выдаст в ответ -- решение работает.
Функции для очистки интервалов я вынес в отдельный модуль =intervals_cleaning.py=
Функции для очистки интервалов я вынес в отдельный модуль =intervals_cleaning.py=, функции для поиска пересечений в модуль =intervals_intersection.py=.
Также в данной тестовой задаче использовать ООП я посчитал излишним, и ограничился функциональным подходом. Но если бы стояла задача решать в ООП-стиле, то ввел бы объекты:
Также в данной тестовой задаче использовать ООП я посчитал излишним. Но если бы стояла задача решать в ООП-стиле, то ввел бы объекты:
- interval (dataclass)
- IntervalLib для инкапсуляции методов по нормализации интервалов

67
task_3/intervals_cleaning.py

@ -0,0 +1,67 @@
"""Модуль, содержащий функции для чистки интервалов.
Под 'чисткой' подразумевается:
- удаление меньших интервалов, полностью входящих в большие (поглощение)
- расширение пересекающихся интервалов (сумма интервалов)
"""
def absorb_small_included_intervals(intervals: list) -> list:
"""Возвращает список интервалов без вложенных интервалов.
Вложенный интервал - интервал меньшего размера, полностью входящий
в больший.
Стоит учитывать, что функция работает только если интервалы
совпадают по началу или концу, если интервалы входят друг в друга
частично (пересекаются) -- здесь мы их не обрабатываем.
"""
# запоминаем самый перевый интервал, раньше него интервалов не
# будет. Если текущий интервал входит в последний добавленный
# нами, т.е. он заканчивается раньше, чем тот который мы добавили,
# то мы его не добавляем в результат, так как он вложенный
big_intervals = [intervals[0]]
for i in range(1, len(intervals)):
if big_intervals[-1][1] >= intervals[i][1]:
continue
else:
big_intervals.append(intervals[i])
return big_intervals
def merge_intersected_intervals(intervals) -> list:
"""Возвращает расширенные интервалы.
Расширенные интервалы -- это интервалы, состоящие из суммы
частично пересекающихся интервалов."""
# запоминаем самый первый интервал. Проходимся по списку, и если
# начало текущего интервала меньше, чем конец последнего
# добавленного, то мы расширяем записанный интервал до конца
# текущего. В противном случае просто добавляем текущий интервал в
# итоговый список интервалов.
res = [intervals[0]]
for i in range(1, len(intervals)):
if res[-1][1] >= intervals[i][0]:
res[-1][1] = max(intervals[i][1], res[-1][1])
else:
res.append(intervals[i])
return res
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)]
# убираем вложенные интервалы
n_intervals = absorb_small_included_intervals(intervals_)
# расширяем пересекающиеся интервалы
result = merge_intersected_intervals(n_intervals)
return result

48
task_3/intervals_intersection.py

@ -0,0 +1,48 @@
"""Модуль поиска пересечений интервалов."""
def has_intersection(interval_A, interval_B) -> bool:
"""Пересекаются ли интервалы."""
if interval_A[1] <= interval_B[0]:
return False
if interval_B[1] <= interval_A[0]:
return False
return True
def get_intersection(interval_A, interval_B) -> list:
"""Возвращает пересечение двух интервалов, если они не
пересекаются, возвращает пустой интервал."""
if not has_intersection(interval_A, interval_B):
return []
begin = max(interval_A[0], interval_B[0])
end = min(interval_A[1], interval_B[1])
return [begin, end]
def get_all_intersections(intervals_A, intervals_B) -> list:
"""Принимает два списка интервалов, для которых нужно найти
пересечения. Возвращает список найденных пересечений."""
# задаем индексы для каждого из интервалов
p_A, p_B = 0, 0
res = []
# Если текущий интервал в списке intervals_A заканчивается позже,
# чем текущий интервал в списке intervals_B, нам нужно перейти на
# следующий интервал в списке intervals_B. Таким образом по
# спискам мы пройдем всего лишь раз.
while p_A < len(intervals_A) and p_B < len(intervals_B):
A, B = intervals_A[p_A], intervals_B[p_B]
if has_intersection(A, B):
res.append(get_intersection(A, B))
if A[1] <= B[1]:
p_A += 1
else:
p_B += 1
return res

36
task_3.py → task_3/task_3.py

@ -1,6 +1,7 @@
"""Задание на расчет интервалов, и вычисление времени, которое ученик
и учитель присутствовали на уроке одновременно."""
from intervals_cleaning import normilize_intervals
from intervals_intersection import get_all_intersections
tests = [
{
@ -73,33 +74,6 @@ tests = [
]
def get_intersection(interval_A, interval_B) -> list:
"""Возвращает пересечение двух интервалов.
Пересечение двух интервалов -- это наибольшее из начал A и B, и
наименьшее из концов A и B."""
# если один из интервалов пустой, то пересечение будет пустым
if not interval_A or not interval_B:
return []
# если какой-то из интервалов имеет нулевую длину (начало=конец),
# то пересечение пустое
if interval_A[0] == interval_A[1] or interval_B[0] == interval_B[1]:
return []
# нужно проверить, что интервалы вообще пересекаются
# А и Б не пересекаются, если
# начало А > конец Б, или если начало Б > конец А
if interval_A[0] > interval_B[1] or interval_B[0] > interval_A[1]:
return []
# начало интервала это наименьшее из начал интервалов А и Б
# конец интервала это наименьший из концов интервалов А и Б
interval_begin = max(interval_A[0], interval_B[0])
interval_end = min(interval_A[1], interval_B[1])
return [interval_begin, interval_end]
def appearance(intervals) -> int:
"""Возвращает время одновременного присутствия ученика и учителя на
уроке в секундах."""
@ -108,15 +82,11 @@ def appearance(intervals) -> int:
tutor = normilize_intervals(intervals["tutor"])
# получаем все интервалы студента на уроке
pupil_on_lesson = [
get_intersection(lesson, p) for p in pupil if get_intersection(lesson, p)
]
pupil_on_lesson = get_all_intersections(pupil, [lesson])
# по очереди сравниваем каждый из интервалов преподавателя с
# каждым из интервалов пересечения студента на уроке
tutor_on_pupil = [
get_intersection(pol, t) for t in tutor for pol in pupil_on_lesson
]
tutor_on_pupil = get_all_intersections(pupil_on_lesson, tutor)
# на данном этапе мы уже имеем интересующие нас интервалы
# (пересечение ученик-учитель-урок); остается узнать

158
task_3/task_3_tests.py

@ -0,0 +1,158 @@
from task_3 import has_intersection, get_intersection
def test_case_1():
test = [
[1594705106, 1594706480],
[1594705158, 1594705773],
[1594705849, 1594706480],
]
result = [
[1594705106, 1594706480],
[1594705158, 1594705773],
]
# print(f"{test=}")
answer = enlarge_elapsed_intervals(test)
assert answer == result, f"Error in test_case_1:\n{answer} \n {result}"
def test_case_2():
test = [
[1594705106, 1594706480],
[1594705106, 1594706490],
[1594705849, 1594706495],
]
result = [
[1594705106, 1594706490],
[1594705849, 1594706495],
]
# print(f"{test=}")
answer = enlarge_elapsed_intervals(test)
assert answer == result, f"Error in test_case_1:\n{answer}\n {result}"
def test_case_3():
test = [
[21, 30],
[25, 32],
[33, 40],
[33, 35],
[41, 45],
[42, 43],
[44, 45],
[46, 50],
[47, 51],
]
result = [
[21, 32],
[33, 40],
[41, 45],
[46, 51],
]
answer = []
for i in range(len(test)):
for k in range(i, len(test)):
a = find_inner_sum_of_intersections(test[i], test[k])
answer.append(a)
print(f"{answer=}")
answer = [a for a in answer if a]
test.extend(answer)
print("!___")
print(test)
answer = enlarge_elapsed_intervals(test)
print(f"{answer=}")
assert answer == result, f"Error in test_case_1:\n{answer}\n {result}"
def test_case_4():
test = [
[21, 30],
[33, 40],
[33, 35], #
[41, 45],
[42, 43], #
[44, 45], #
]
result = [
[21, 30],
[33, 40],
[41, 45],
]
answer = absorb_small_included_intervals(test)
assert answer == result, f"Error in test_case_4:\n{answer}\n {result}"
def test_case_5():
test = [
[21, 30], #
[28, 40], #
[33, 35],
[41, 45],
[42, 43],
[44, 45],
[53, 60],
[51, 55],
]
result = [
[21, 40],
[41, 45],
[51, 60],
]
answer = get_extended_intervals(test)
assert answer == result, f"Error in extend_intervals:\n{answer}\n {result}"
def absorb():
test = [
[21, 30],
[33, 40],
[33, 35], #
[41, 45],
[42, 43], #
[44, 45], #
]
result = [
[21, 30],
[33, 40],
[41, 45],
]
test_b = {i[0]: i[1] for i in test}
beg = [test[k] for k, v in enumerate(test_b) if test[k][1] < test_b[v]]
test_e = {i[1]: i[0] for i in test}
end = [test[k] for k, v in enumerate(test_e) if test[k][0] > test_e[v]]
print(beg, end)
def inters():
print(has_intersection([21, 25], [22, 30]))
print(has_intersection([22, 30], [21, 25]))
print(has_intersection([21, 25], [26, 30]))
print(has_intersection([26, 30], [21, 25]))
def inters2():
print(get_intersection([21, 25], [22, 30]))
print(get_intersection([22, 30], [21, 25]))
print(get_intersection([22, 30], [31, 33]))
if __name__ == "__main__":
# test_case_1()
# test_case_2()
# test_case_3()
# test_case_4()
# test_case_5()\
# absorb()
# inters()
inters2()
Loading…
Cancel
Save