Кой е най-бързият/лесният начин за преброяване на активните потребители през последната минута?

Работите за Zynga и искате да преброите броя на текущо активните играчи за различни игри. Вашият уеб сървър обработва ping от много различни игри и всеки потребител има уникален GUID. Трябва да можете да заявявате брой активни потребители за една игра наведнъж. Активните потребители са тези, които са получили ping в последната минута.

Редовете на регистрационния файл влизат непрекъснато в уеб сървър:

10.1.12.13 - - "http://zynga.com/ping?guid=<guid>&game=<gameID>" -

Кой е най-бързият/лесният начин за преброяване на активните потребители? Моля, предложете 45 минути отговор с някакъв код.


Моята версия

// web server interface, every time ping comes in count() will be called
// void count(String gameId, String guid)
// int getNumberActivePlayers(String gameId)

struct Record{
  String gameID;
  String guid;
};

class PingStorage{
private:
  max_heap<long, Record> storage;
public:
  //    O(log(n))
  //  n = total number of elements in storage
  void count(String gameId, String guid){
    long currentTimeStamp = getUnixTimeStamp();
    Record rec ;
    rec.gameId = gameId;
    rec.guid = guid;
    storage.add(currentTimeStamp, rec);
  }
  //N = numner of records in last ,minutes in storage
  //O(N)
  int getNumberActivePlayers(String gameId){
    map<String, Set<string> > game2user;
    long tillTimeStamp = getUnixTimeStampNow() - 60;
    while(true){
      pair<long, Record> rec = storage.getMax(); //O(1)
      if(rec.first <= tillTimeStamp) break;  
      Set<String> temp = game2user[rec.gameid]; //O(1)
      temp.add(rec.userid); //O(log(N)) - O(1)
    }
    return game2user[gameID].size();
  }
};

person Master Yoda    schedule 14.06.2012    source източник
comment
getMax() ще се върне и ще премахне max елемента от купчината.   -  person Master Yoda    schedule 14.06.2012
comment
Какво е забавянето, след което потребителят не се счита за активен, ако не е получена заявка за този guid? (нямам нужда от реалния номер, NDA yadda^2)   -  person Christopher Mahan    schedule 14.06.2012
comment
тогава мисля, че това е O(logN) операция, така че общото ви време става O(NlogN)   -  person xvatar    schedule 14.06.2012
comment
@xvatar прав си :)   -  person Master Yoda    schedule 14.06.2012
comment
Предполагам, че това е въпрос от интервю за Zynga?   -  person cheeken    schedule 15.06.2012
comment
Моля, можете ли да редактирате решението си и да го публикувате като отговор?   -  person Colonel Panic    schedule 15.06.2012


Отговори (4)


Ако приемем, че това е решение в реално време, можете да обработвате ping заявки в O(1), да генерирате текущата статистика на играча в O(1) и да използвате O(num_player) пространство, като жертвате известна точност. Ключът е да се дискретизират времената.

Общ преглед

Основната идея е да се представят дискретни времеви интервали като обекти и да се съхранява в тези обекти следното свойство: броя на отделните играчи, които са пингували през този времеви интервал, които не са пингвали след това. За да направите заявка за броя на активните потребители, изчислете претеглена сума от последните x времеви интервали, които съставляват последната минута.

Подробности

Първо изберете приемлива времева разделителна способност. В този пример избирам интервали от 15 секунди.

Поддържайте пет структури от данни PingInterval, за да представите пет от тези интервали (обхващащи още 1 интервал от 1 минута). PingInterval съдържа едно свойство: брояч. Тези PingIntervals се поддържат в PingMonitor. Всеки път, когато играч пингва, актуализирайте карта в PingMonitor, която картографира всеки играч към текущия интервал от време. Когато извършвате това картографиране, направете следните стъпки, които поддържат броя в рамките на PingIntervals (според характеристиките, които описах в раздела за преглед).

  • Ако играчът вече е картографиран към интервал и това е текущият интервал, не правете нищо.
  • Else, if the player is mapped to an interval that isn't the current interval,
    • decrement the old interval's count,
    • увеличаване на броя на текущия интервал,
    • и съпоставете този играч с този интервал.
  • Else, if the player is not mapped to an interval at all,
    • increment the current interval's count,
    • карта на играча към текущия интервал.

(Ако PingInterval за представяне на текущия час все още не съществува, задайте най-стария PingInterval на нула, създайте нов PingInterval по безопасен за нишката начин и след това продължете както обикновено.)

Когато искате да направите заявка за броя на активните потребители, изчислете претеглена сума от последните пет интервала от време. Например, ако сте само на 5 секунди от текущия времеви интервал (което означава, че следващите 10 секунди от този интервал все още не са се случили), изчислете тази стойност: 2/3 * най-стар интервал + сбор от 4-те най-нови интервала.

Други мисли

Пет интервала е много консервативно; бихме могли значително да увеличим броя за по-голяма точност (може би един за секунда) и пак ще ни осигури значителни спестявания. Важната част е, че нашето време сега е дискретни интервали. Това означава, че когато преброим броя на активните потребители, не е нужно да разглеждаме всяко отделно време (което е равно на броя на потребителите); вместо това можем да разгледаме x интервали от време, които сме предварително дефинирали.

person cheeken    schedule 14.06.2012
comment
не трябва ли вашите оценки на сложност за ping и заявка всъщност да бъдат O(num_bins)? - person moooeeeep; 15.06.2012
comment
@moooeeeep Pinging не преминава през контейнери, така че е O(1). Прав си обаче, че сложността на заявката е линейно пропорционална на броя на контейнерите. Въпреки това, тъй като броят на контейнерите е предварително дефинирана константа, аз наричам неговата сложност константа време - особено след като това всъщност не е вход към алгоритъма. - person cheeken; 15.06.2012
comment
Сигурно съм разчел погрешно нещата с добавянето на нови записи. Продължавате да създавате нови кошчета, вместо да премествате GUID записите през контейнерите с течение на времето... - person moooeeeep; 15.06.2012
comment
Това не е решението. Да кажем, че ви предаваме масив от gameid и вие трябва да дадете брой активни потребители за всеки gameid. Как ще се мащабира вашето решение в тази ситуация?? - person Master Yoda; 17.06.2012
comment
@MasterYoda Процесът, който описвам, е решение за една игра. За няколко игри просто приложете решението няколко пъти. - person cheeken; 17.06.2012
comment
@cheeken да предположим, че не знаете броя на игрите предварително. Имате нужда от структура от данни, която поддържа карта между gameid и броя на активните потребители. - person Master Yoda; 17.06.2012
comment
@MasterYoda Бих поддържал хеш-таблица между GUID на играта и PingMonitor. Когато дойде заявка, бих хванал PingMonitor на играта за присвояване и бих извикал операцията върху него. - person cheeken; 17.06.2012
comment
@cheeken Все пак вашето решение не е достатъчно ефективно. Това означава, че ако имаме 10000 gameid, вие запазвате no_of_time_interval*10000 TimeInterval обекта. Можете ли да комбинирате всички gameid в една и съща структура от данни. - person Master Yoda; 18.06.2012
comment
@MasterYoda Какво количество пространство би било достатъчно ефективно? Не можете да комбинирате структурите от данни за отделни игри; ако го направихте, тогава броят на играчите ще бъде общ за всички игри, а не за отделни игри. - person cheeken; 18.06.2012

Моят подход би бил да използвам deque (наречена опашка в останалата част от тази публикация), към която се насочват всички GUID, които се наблюдават, т.е., която по този начин е подредена по възраст. Освен това бих използвал hashmap, който съдържа указателите към записите на всеки GUID, присъстващ в опашката.

Когато нов GUID бъде избутан към опашката, старият запис (ако има такъв) ще бъде потърсен в hashmap, премахнат от опашката и новият вместо това ще бъде присвоен на hashmap.

С течение на времето всички записи в опашката, които надхвърлят възрастовия праг, ще бъдат извадени (и премахнати от hashmap).

Дължината на опашката (известна още като брой активни потребители) може да се следи като отделна променлива, за да се избегне прескачане през опашката за всяка заявка.

За да поддържате множество игри, просто добавете такава структура за всеки gameID.

Сложност: O(1) вмъкване/изтриване на наблюдения (при перфектен хеш, т.е. без сблъсъци), O(1) заявка, O(n) интервал.

person moooeeeep    schedule 14.06.2012
comment
Можете ли да уточните как и кога остарелите елементи се премахват от опашката? Ако можете да направите това в O(1), това звучи като идеалното решение. (Но мисля, че може да не е толкова лесно!) - person cheeken; 15.06.2012
comment
@cheeken Предполагам, че времевите марки винаги се увеличават, следователно опашката е в ред (най-старият запис в началото, най-младият отзад). Всеки път, когато възникне актуализация или заявка, GUID, които са по-стари от една минута, ще бъдат извадени (O(1)) и премахнати от хеш картата (O(1), при перфектен хеш). Броячът е съответно адаптиран. Мисля, че е доста ясно. - person moooeeeep; 15.06.2012
comment
Когато се наблюдава GUID, който вече е в опашката, предишният запис се намира чрез hashmap и се премахва от опашката (O(1)), преди нов запис да бъде избутан в опашката. - person moooeeeep; 15.06.2012
comment
Разбирам, предположих, че така го правиш. Но защо премахването на GUID, които са по-стари от минута, е O(1)? Броят на GUID, които трябва да премахнете, е пропорционален на броя на играчите. Дори ако можете да намерите къде в опашката искате да започнете да премахвате играчи и просто прекъснете цялата тази опашка, пак трябва да преброите броя на елементите в нея, за да актуализирате броя, нали? Просто се уверявам, че не пропускам нещо тук. - person cheeken; 15.06.2012
comment
@cheeken имах предвид само изтриването на всеки отделен GUID. Премахването на n_stale_guids GUID струва разбира се O(n_stale_guids). Вероятно наистина има място за подобрение... - person moooeeeep; 15.06.2012
comment
@cheeken Операцията за премахване на остарели записи може да се случи на интервал от 1 минута. Така че мисля, че целият алгоритъм се амортизира до O(1), ако дейностите по пингване са достатъчно интензивни. Освен това изглежда, че записите в опашката са сортирани по времево клеймо, така че можем да използваме двоична сонда, за да определим позицията, където искаме да изрежем опашката. Някаква мисъл? - person KFL; 16.06.2012
comment
@Kay За решение в реално време изчистването на записи на интервали от 1 минута не е достатъчно. Помислете за състоянието на деопашката 1 секунда преди прочистване. По това време опашката съдържа записи на стойност 1 минута 59 секунди. Това е почти два пъти стойността на действителния брой играчи. Ако в този момент се извърши преброяване, то ще бъде почти два пъти по-голямо от действителната стойност. - person cheeken; 16.06.2012

РЕДАКТИРАНЕ: Предположих, че този въпрос не е за получаване на отговор в реално време за въпроса „колко потребители са активни сега“, а по-скоро за получаване на исторически стойности – колко потребители са били активни в 15:25 . Запазвам старото решение под новото:

И така, искате да знаете колко потребители са активни сега, поддържайте опашка за игра. Всеки път, когато видите нов запис в журнала, разберете към коя игра принадлежи и го добавете към опашката на играта. След всяко добавяне изчистете старите записи в началото на опашката (всички записи, по-стари от 1 минута към момента на почистването).

Когато бъдете попитани за броя на активните потребители в дадена игра, направете същото почистване на опашката на играта и върнете дълбочината на опашката.

Съхранявайте хеш, съпоставящ игрите към опашките, и получавате операция O(N), като N е броят на редовете в дневника - всеки ред се обработва най-много два пъти - веднъж за добавянето му, веднъж за премахването му. Също така правите допълнително сравнение за всяко добавяне и търсене (когато решите, че запис в опашката не е достатъчно стар), но това е постоянно време по N. Така че общо O(N).

Предишен отговор на този друг въпрос: Виждайки, че няма толкова много минути (1440 на ден), бих създал вектор за всяка игра, със слот за всяка минута.

Прегледайте лог файла, за всеки ред вземете времето, закръглете го до най-близката минута и добавете 1 към съответния слот в масива. След като приключите, ще знаете точно колко активни потребители са били за всяка игра във всяка минута.

Сложност - O(N), като N е броят на редовете в лог файла.

За да поддържате множество игри, просто използвайте хеш за картографиране от името на играта към нейния вектор.

Сега това предполага, че проверявате за активни потребители само в рамките на цялата минута (1:00:00, 1:01:00 и т.н.). Това вероятно е нещото, което трябва да направите така или иначе.

person zmbq    schedule 14.06.2012
comment
разбирате това погрешно. Да предположим, че заявката е направена в 12:15:30, ще трябва да проверите и двата елемента в 12:14 и 12:15, което може да бъде log(n) в най-лошия случай. Това е толкова добро, колкото и най-простият алгоритъм за съхраняване на всички стойности в един масив и извършване на двоично търсене в него. + простото използване на брояч няма да ви даде правилния отговор. - person Samy Arous; 14.06.2012
comment
Всъщност търсенето се извършва в O(1) - 12:14 е клетка номер 14, 12:15 е клетка номер 15, 2:30 е клетка номер 150 (60 * час + минута). - person zmbq; 14.06.2012
comment
имам предвид, че трябва да получите всички елементи между 12:14:30 и 12:15:30, не можете просто да съберете и двете клетки (14,15). - person Samy Arous; 14.06.2012
comment
Наистина. Но предполагам, че няма да има нужда, той трябва да каже колко потребители са били активни всяка минута в миналото, така че трябва да изберете граници. Ако той просто иска да знае колко потребители са активни сега, това е съвсем друг въпрос, който също е много прост - просто поддържайте опашка за всяка игра, натискайте записи, когато получите ping, и почиствайте твърде стари записи. - person zmbq; 14.06.2012
comment
Това е вторият случай: Трябва да можете да заявявате брой активни потребители за една игра наведнъж. и да, това би било решението, за което си мислех. Използване на опашка. - person Samy Arous; 14.06.2012
comment
Добре, модифицирах отговора си, за да отговаря и на двата случая. - person zmbq; 14.06.2012

Това ще бъде моята последователност от отговори:

  1. Защо да се занимавам? Най-лесно е да преброите минута по минута колко потребители са активни. Не е ли достатъчно да знаете това?
  2. Ако наистина държите на актуалната информация, нека броим в кошчета секунда по секунда (както е описано от Cheeken). Това ще бъде с точност до част от секундата.
  3. Добре, ако точността в реално време е „необходима“ и искате да ме интервюирате за структурите на данни, нека използваме куп клиенти, оценени по последно време на активност (както е описано от учителя Йода).
  4. Ако е необходима точност в реално време и трябваше да направим това в производството, нека използваме сървъра за структура на данни Redis. Ние поддържаме сортиран набор от клиенти, оценени по време на последното действие. Можем да направим запитване колко клиенти са били активни в последната минута или в последния час с командата zcount. Това е полезно и надеждно.
person Colonel Panic    schedule 15.06.2012