У нас есть система входа в систему, которая отслеживает, как долго люди находятся на связи. Я хотел бы написать код, чтобы найти людей, которые были в сети одновременно. Посмотрите на этот пример, пожалуйста:
P1: [1,7]
P2: [2,5]
P3: [3,4]
P4: [6,8]
Думайте об этом как об интервалах от человека 1 до 4. Я хочу, чтобы результат алгоритма был примерно таким:
P1, P2 : [2, 3]
P1, P2, P3 : [3, 4]
P1, P2 : [4, 5]
P1, P4 : [6,7]
Я попытался решить проблему с двумя циклами for, чтобы мы получили список людей, чьи интервалы перекрываются, но проблема заключается в том, чтобы иметь дело с интервалами для более чем одного человека. например, в приведенном выше примере [3,4] не обязательно должно находиться в [4, 5] в строке три, потому что он рассчитывается как интервал для трех человек.
Этот подход должен работать с минимальной дополнительной бухгалтерией: stackoverflow.com/questions/15013800/… (дополнительная бухгалтерия заключается в том, что вместо того, чтобы отслеживать количество, отслеживать отдельных лиц)
Закодируйте все границы интервала в виде 1:+P1
, 7:-P1
. Затем отсортируйте все границы в хронологическом порядке и просканируйте по возрастанию времени. Также обновляйте список лиц, присутствующих при «выполнении» операций вставки или удаления.
{} 1:+P1 {P1} 2:+P2 {P1, P2} 3:+P3 {P1, P2, P3} 4:-P3 {P1, P2} 5:-P2 {P1} ...
Каждая конфигурация возникает между одной временной привязкой и следующей.
Некоторые конфигурации могут со временем повторяться, но вы не указали, как с ними работать. При указанном выше методе они будут перечислены отдельно.
Один подход, предполагающий, что ввод является словарем человека -> интервал:
from collections import defaultdict
from itertools import pairwise
data = {"P1": (1, 7),
"P2": (2, 5),
"P3": (3, 4),
"P4": (6, 8)}
table = defaultdict(list)
for key, (start, end) in data.items():
for i, j in pairwise(range(start, end + 1)):
table[(i, j)].append(key)
result = {k: v for k, v in table.items() if len(v) > 1}
for key, value in result.items():
print(value, key)
Выход
['P1', 'P2'] (2, 3)
['P1', 'P2', 'P3'] (3, 4)
['P1', 'P2'] (4, 5)
['P1', 'P4'] (6, 7)
Он такой маленький, но я использую временные метки, и этот код требует много времени для моего кода. Однако я благодарю вас за ваш ответ. Я не знал об itertools в python. Это так полезно.
В значительной степени вдохновлен Минимальным количеством платформ, необходимым для железнодорожной станции, но с сохранением набора присутствующих людей вместо количества присутствующих поездов:
Также реализация алгоритма намекает в ответе Ива Дауста.
Идея состоит в том, чтобы сначала построить хронологический список всех событий, где событием является либо приход, либо уход человека.
Затем мы повторяем список событий, сохраняя набор присутствующих людей, добавляя прибывающих и удаляя уходящих.
Ниже хронологический список событий реализован как время сопоставления Python dict
с событиями Python list
, так что события, происходящие в одно и то же время, если таковые имеются, естественным образом группируются вместе.
from operator import itemgetter
data = {
'P1': [1,7],
'P2': [2,5],
'P3': [3,4],
'P4': [6,8]
}
def get_overlaps(data):
events = {}
for person, (start, end) in data.items():
events.setdefault(start, []).append((person, set.add))
events.setdefault(end, []).append((person, set.remove))
# events = {1: [('P1', add)], 2: [('P2', add)], 3: [('P3', add)], 4: [('P3', remove)], 5: [('P2', remove)], 6: [('P4', add)], 7: [('P1', remove)], 8: [('P4', remove)]}
present_persons = set()
start_time = 0
for new_time, grouped_events in sorted(events.items(), key=itemgetter(0)):
if len(present_persons) >= 2:
yield (frozenset(present_persons), (start_time, new_time))
start_time = new_time
for new_person, add_or_remove in grouped_events:
add_or_remove(present_persons, new_person)
data = {
'P1': [1,7],
'P2': [2,5],
'P3': [3,4],
'P4': [6,8]
}
overlaps = list(get_overlaps(data))
print(overlaps)
# [(frozenset({'P2', 'P1'}), (2, 3)),
# (frozenset({'P2', 'P1', 'P3'}), (3, 4)),
# (frozenset({'P2', 'P1'}), (4, 5)),
# (frozenset({'P4', 'P1'}), (6, 7))]
data2 = { # three events at time 5: (P1,remove), (P2,remove), (P4,add)
'P1': [1,5],
'P2': [2,5],
'P3': [3,4],
'P4': [5,8]
}
overlaps = list(get_overlaps(data2))
print(overlaps)
# [(frozenset({'P2', 'P1'}), (2, 3)),
# (frozenset({'P2', 'P1', 'P3'}), (3, 4)),
# (frozenset({'P2', 'P1'}), (4, 5))]
Этот алгоритм имеет временную сложность O(N), где N — количество человек. Для сравнения, алгоритм в ответе Дани Месехо имеет временную сложность O (TN), где N — количество людей, а T — максимальное время присутствия любого человека, измеряемое количеством мгновений.
Спасибо за отличный код и решение. Я узнал хорошие вещи из этого. У меня есть несколько вопросов, почему вы использовали замороженный набор? а почему вы написали это как генератор? Влияют ли они на время выполнения? Я использую библиотеку datetime для времени, и ваша реализация очень быстрая.
Если хотите, вы можете использовать set вместо FrozenSet, но важно убедиться, что никакие два множества не являются одним и тем же объектом. Попробуйте следующий код: def f(): a = set(); for x in range(4): a.add(x); yield a
, а затем print(list(f()))
. Вместо [{0}, {0,1}, {0,1,2}, {0,1,2,3}]
, как вы могли ожидать, будет напечатано [{0,1,2,3}, {0,1,2,3}, {0,1,2,3}, {0,1,2,3}]
. А если вы сделаете def g(): a = set(): for x in range(4): a.add(x); yield frozenset(a)
, то получите то, что ожидаете.
Я написал его как генератор, потому что обычно проще писать функции таким образом на python, но вы можете легко написать его как функцию, которая вместо этого возвращает список, добавив result = []
перед циклом и заменив yield ...
на result.append(...)
в цикле, и добавление return result
после цикла.
Не могли бы вы опубликовать свой код? Это поможет лучше понять ваш фактический вклад...