Оптимизационная задачка
Jun. 13th, 2013 05:23 pmУж как переложил на бытовые реалии, так переложил.
Имеется поликлиника, в которых работают специалисты N разных профилей. Приходящие пациенты встают в общую очередь. Сами они объяснить, какой врач им нужен, не могут, а в ответ на вопрос "К такому-то есть кто-нибудь?" из очереди выходит первый нуждающийся в этом враче, или никто не выходит, если таких на данный момент нет. Время, затрачиваемое регистратурой на один вопрос, в обоих случаях одинаково.
Тривиальный принцип работы регистратуры - называть всех специалистов по кругу, но при "эпидемии" это очевидным образом неэффективно.
Требуется придумать адаптивный алгоритм, стремящийся максимизировать пропускную способность регистратуры, в т.ч. и в условиях изменения характера "эпидемий", сохраняя справедливость: стоящий первым в очереди никогда не должен ждать дольше, чем M вопросов.
Начинаем с колоды из N карточек с номерами от 1 до N. Далее играем раунды так:
Выкликаем согласно номерам на карточках, и раскладываем карточки в N пар кучек, в каждой паре справа - успешные вызовы (Si), слева - неудачные (Fi).
Когда колода исчерпалась, смотрим: если во всех N парах было хотя бы по одному успешному вызову (все Si > 0), то набор Si более или менее неплохо моделирует соотношение частот клиентов врачей, а для уточнения соотношения на следующем круге составляем колоду из 2*Si карточек каждого типа, если это не больше, чем M, иначе по Si каждого типа.
Если же оказалось, что какие-то Si равны нулю, то это значит, что частота запросов для этих i меньше, чем 1/K, где K - размер колоды на только что закончившемся круге. Тогда составляем колоду из min(M, 2*K) карточек так, чтобы в них было по одной карточке для тех i, для которых Si было = 0, а остальные были в той же пропорции, что и ненулевые Si.
Из полученной новой колоды отбираем по одной карточке каждого типа, кладем в порядке возрастания в начало колоды, остальные тасуем и кладем за ними.
Споласкиваем, повторяем.
no subject
Date: 2013-06-14 01:58 am (UTC)no subject
Date: 2013-06-14 02:04 am (UTC)no subject
Date: 2013-06-14 02:15 am (UTC)no subject
Date: 2013-06-14 04:02 am (UTC)no subject
Date: 2013-06-14 01:08 pm (UTC)Пусть N=10, M=20. Очевидно, что если регистратор будет просто задавать вопросы "кто к врачу 1, кто к врачу 2,..., кто к врачу 10" - то первый в очереди к любому врачу не будет ждать дольше 10 вопросов, что, очевидно, меньше 20
no subject
Date: 2013-06-14 02:37 pm (UTC)no subject
Date: 2013-06-14 03:00 am (UTC)Проще говоря, допустим, M/N=10, тогда 10 раз подряд называем каждого специалиста по очереди (либо, если в какой-то момент не оказалось желающего, переходим к следующему специалисту).
no subject
Date: 2013-06-14 02:40 pm (UTC)no subject
Date: 2013-06-14 03:12 pm (UTC)То есть нужно хранить коэффициент для каждого специалиста. Если число желающих совпало с M/N, добавить к коэффициенту "свободные вызовы", если число желающих меньше, убавить коэффициент до количества вызовов (а "остаток" добавить к "свободным").
Опять пример: M/N=10.
Вызываем 1-го, желающих - 3. Коэффициент К1==3. Свободный увеличиваем на 7 (изначально он равен 0).
Вызываем 2-го, желающих - 7, К2==7, к свободным добавляем 3.
Вызываем 3-го, желающих - 10, по одному изымаем из свободных дополнительные вызовы до тех пор, пока либо не кончатся желающие, либо кончатся вызовы.
Вызываем 4-го, желающих - 0, К4==1 (1- минимальное значение коэффициента). К свободным добавляем 9.
Ну и далее по кругу... Если вдруг предполагается несколько эпидемий, то из свободных на каждом этапе нельзя выбирать все вызовы, а только половину, например.
no subject
Date: 2013-06-14 03:26 pm (UTC)no subject
Date: 2013-06-14 04:30 am (UTC)Пусть popularity(k) - сколько пациентов к k-му врачу стоит в очереди (допустим какой-то оракул знает ответ).
Сортируем врачей по лексикографическому возрастанию тупла (max(M-a(i), N), -popularity(k)), и выбираем первого.
Этот алгоритм будет эффективно приоритезировать: самый популярный врач будет обслуживаться максимально, за исключением тех врачей, которых надо назвать для соблюдения справедливости. При одинаковой популярности, трафик будет делиться поровну между всеми одинаковыми.
Допустим, мы не знаем, чему равен popularity(k). Эту популярность надо *измерить*.
Измерять её можно по-разному. Вижу две крайности:
1. "Мочить" одного врача до упора, пока не кончатся его клиенты, или пока его не вытеснят ребята, которые по справедливости. Грубо говоря, popularity(k) = 1 если на предыдущем шаге был хотя бы один клиент, иначе 0. Кстати, в этом случае если к ребятам по справедливости кто-то пришёл, то в следующем раунде они уже будут соревноваться с первым и делить с ним трафик. Но те их них, которые "кончатся", отвалятся из соревнования. В этом случае у нас такой расклад: для лузеров, к которым никого нет, будет задан один вопрос на каждые M шагов. Для тех, к кому за M-шаговый цикл пришло 5 человек, будет 5 или 6 вопросов (в зависимости от того, люди исчерпались до или после обработки лузеров).
Можно делать какую-то адаптивную политику и учитывать историю, например, не присваивать единицу в популярность, а прибавлять единицу, а в случае неудачи не обнулять, а вычитать или делить. Если мы знаем паттерны эпидемий, можно подтюнить формулы так, чтобы было счастье.
2. Можно давать каждому врачу вплоть до M/N попыток и запоминать количество успешных. Круг может закончиться раньше, чем через M шагов, если к кому-то было менее M/N пациентов. На второму круге эти числа уже можно использовать как популярность. Время от времени делать тюнинг, заново пересчитывая. Или учитывая длины новых отрезков в формулу популярности. В случае одного доминирующего и остальных отдыхающих, его throughput будет заметно меньше, т.к. он будет получать не (M-N)/M, а (M/N)/(M/N + N). Зато будет некоторый QoS, честнее пропорция трафика и быстрее реакция на появление новых популярных врачей.
За неимением дополнительных знаний об эпидемиологических свойствах, я бы выкатил в продакшн п.1., ибо это максимизирует max throughput для лучшего, что есть нередко важнейшая метрика.
no subject
Date: 2013-06-14 02:47 pm (UTC)no subject
Date: 2013-06-14 03:45 pm (UTC)no subject
Date: 2013-06-14 04:56 pm (UTC)Ещё вопрос: *зачем* минимизировать количество магических чисел в параметрах?
Ещё вопрос: вы, с одной стороны, говорите, что 50% лишних вопросов это плохо, а, с другой, намекаете, что п.2. лучше, чем п.1., когда при определённом паттерне входящих, п.1. будет производить намного меньше лишних вопросов, чем п.2. Т.е. вы оцениваете, держа в голове определённый паттерн. Каков он? Для каких данных мы оптимизируем?
no subject
Date: 2013-06-14 05:09 pm (UTC)Нужно, чтобы один и тот же алгоритм минимизировал количество лишних вопросов как при более или менее равномерном распределении, так и при сильно смещенном распределении, и адаптировался достаточно быстро.
no subject
Date: 2013-06-14 05:17 pm (UTC)no subject
Date: 2013-06-14 05:39 pm (UTC)no subject
Date: 2013-06-18 07:23 am (UTC)Клиенты могут приходить с теми же самыми близкими средними частотами, но вдвое реже, и K/N упадёт вдвое.
no subject
Date: 2013-06-18 02:19 pm (UTC)Вы видели апдейт с нашим решением?
no subject
Date: 2013-06-18 04:24 pm (UTC)Мне надо ещё подумать, чтобы убедить себя, что это работоспособный алгоритм. Потенциально слабое место мне пока видится вот здесь: вероятности обновляются *только* когда 2*Si > M, соответственно, если мы имеем ситуацию, в которой это соотношение consistently не выполняется (например, уже есть эпидемия к 1-2 врачам), то про нового, третьего врача, к которому возникла эпидемия, регистратура никогда не узнает (в смысле, не начнёт увеличивать его вероятности), пока не кончится предыдущая эпидемия. Правильно ли я понял?
Вообще, после всех этих уточнений мне эта задача уже почти не нравится. Она слишком размыта в изначальной формулировке, и слишком сложна (и неуниверсальна) в деталях специфики условия, после того, как вы поделились этими деталями.
no subject
Date: 2013-06-19 08:07 pm (UTC)Нет, почему же? Когда 2*Si > M, то мы делаем следующий раунд из Si, а если не больше, то из 2*Si. Разница будет лишь в том, с какой точностью (с каким знаменателем) мы оценим частоты разных i.
Переключение между эпидемиями происходит так: допустим, N = 5, M = 100, и была эпидемия номер 1. Тогда раунд состоит из 96 окликов 1 (из которых успешных, скажем, 10), и по одной для 2-5.
Если, в каком-то раунде, кроме десяти пациентов типа 1, оказался еще и пациент типа 2, то в следующем раунде типу 2 достанется 1*(100-3)/(10+1) = 9 окликов, что почти покрывает эпидемическую интенсивность прихода пациентов. Таким образом переключение между эпидемиями происходит за два раунда.
no subject
Date: 2013-06-18 04:29 pm (UTC)no subject
Date: 2013-06-19 07:51 pm (UTC)no subject
Date: 2013-06-19 07:57 pm (UTC)Но вы понимаете, что существуют исходные данные + детали критериев оценки, на которых моё решение будет лучше вашего? Если же вы допилите условия до таких, на которых ваше решение будет лучше, то вы, по сути, опишите ваш test data set. Я поэтому и подумал, почему бы не оптимизировать этот процесс и не выложить test data set as is.
no subject
Date: 2013-06-19 08:21 pm (UTC)Наше решение - это, собственно, и есть Ваше решение 2, с удобной конкретизацией "время от времени делать тюнинг, заново пересчитывая". Быстрая адаптивность получается естественным образом.
no subject
Date: 2013-06-14 04:40 am (UTC)no subject
Date: 2013-06-14 04:45 am (UTC)no subject
Date: 2013-06-14 05:46 am (UTC)no subject
Date: 2013-06-14 02:56 pm (UTC)no subject
Date: 2013-06-14 03:52 pm (UTC)Я бы добавил счетчик длины очереди на каждом канале, и при опросе выдавал следующего ждущего и текущую длину очереди. И потом пытался эти очереди балансировать, чаще читая из каналов с более длинной очередью.
no subject
Date: 2013-06-14 04:00 pm (UTC)Если вдуматься, это эквивалентно. Выбор и опрос очереди, из которой вызывать - то же самое, что "К такому-то есть кто-нибудь?"
Добавить в прибор ничего нельзя, да и смысла нет - в sustained near-optimal mode длина очереди в каждом канале - от нуля до двух.
no subject
Date: 2013-06-14 04:07 pm (UTC)То есть, в простейшем виде - при каждом опросе запоминать, какие очереди были пустые, а какие нет, и если были какие-то непустые, то вставлять кург опроса только тех очередей, которые были не пустые.
Эту логику можно расширить на хранение результатов последних N опросов. И делать специальный круг для очередей, которые вернули непусто при последних N опросах, потом при как минимум последних (именно последних, а не с перерывами в истории) N-1 опросах, как минимум N-2, и так далее. Естественно, что как только при каком-то опросе все возвращают "пусто", память сбрасывается.
no subject
Date: 2013-06-14 04:19 pm (UTC)no subject
Date: 2013-06-14 04:33 pm (UTC)Оно мне, кстати, напомнило задачку, которую мне как-то задавали на интервью. Там было про разделение пропускной способности канала между несколькими потоками данных согласно заданным пропорциям, но если некий поток использует меньше лимита, то оставшаяся от него лишняя пропускная способность делится между теми, кому нужно больше.
no subject
Date: 2013-06-14 04:53 pm (UTC)no subject
Date: 2013-06-14 04:41 am (UTC)1. У каждого врача есть лимит на количество вопросов подряд, вначале limit(k) = 2.
2. В случае упирания в limit после цепочки из q вопросов, происходит следующее: если все q были обработаны, то limit := limit * 2 (по сути он будет 2q), а popularity := q. Если же цепочка закончилась в результате облома, и на последний вопрос никто не откликнулся, сбрасываем в начало: limit := 2, popularity := 0.
Получаем ситуацию, в которой у врача-лузера есть шанс постепенно "разогнаться" и потом делить канал почти поровну с предыдущим победителем, пока один из них не исчерпает спрос.
no subject
Date: 2013-06-14 05:11 am (UTC)P.S. Я там где-то мог не учесть +1 или -1, это в юнит тестах отлаживается :)
no subject
Date: 2013-06-14 06:09 am (UTC)no subject
Date: 2013-06-14 11:14 am (UTC)Второй вариант (изоморфный) после направления к специалисту пациент переходит в другую очередь (конкретно к этому специалисту), в которой время уже никак не учитывается и обслуживание идёт в порядке очереди.
no subject
Date: 2013-06-14 02:53 pm (UTC)no subject
Date: 2013-06-14 02:49 pm (UTC)no subject
Date: 2013-06-14 01:24 pm (UTC)no subject
Date: 2013-06-14 03:10 pm (UTC)no subject
Date: 2013-06-14 03:44 pm (UTC)Решение - регистратура самообслуживания. Чтоб каждый пациент самостоятельно классифицировал, к какому врачу ему надо.
То есть, переходя на кнопкодавские реалии, вызывал функцию, назначающаю врача, в контексте своего собственного треда.
no subject
Date: 2013-06-14 03:46 pm (UTC)