Хороший алгоритм обхода графа - PullRequest
11 голосов
/ 24 августа 2009

Абстрактная проблема: у меня есть график около 250 000 узлов, а среднее количество соединений составляет около 10. Поиск соединений узла - это длительный процесс (скажем, 10 секунд). Сохранение узла в базе данных также занимает около 10 секунд. Я могу очень быстро проверить, присутствует ли узел в БД. Допуская параллелизм, но не имея более 10 длинных запросов за один раз, как бы вы пересекали график, чтобы быстрее получить максимальный охват.

Конкретная проблема: я пытаюсь почистить страницы пользователя сайта. Чтобы найти новых пользователей, я выбираю список друзей у уже известных пользователей. Я уже импортировал около 10% графика, но постоянно зацикливаюсь или использую слишком много памяти, помня слишком много узлов.

Моя текущая реализация:

def run() :
    import_pool = ThreadPool(10)
    user_pool = ThreadPool(1)
    do_user("arcaneCoder", import_pool, user_pool)

def do_user(user, import_pool, user_pool) :
    id = user
    alias = models.Alias.get(id)

    # if its been updates in the last 7 days
    if alias and alias.modified + datetime.timedelta(days=7) > datetime.datetime.now() :
        sys.stderr.write("Skipping: %s\n" % user)
    else :
        sys.stderr.write("Importing: %s\n" % user)
        while import_pool.num_jobs() > 20 :
            print "Too many queued jobs, sleeping"
            time.sleep(15)

        import_pool.add_job(alias_view.import_id, [id], lambda rv : sys.stderr.write("Done Importing %s\n" % user))

    sys.stderr.write("Crawling: %s\n" % user)
    users = crawl(id, 5)
    if len(users) >= 2 :
        for user in random.sample(users, 2) :
            if (user_pool.num_jobs() < 100) :
                user_pool.add_job(do_user, [user, import_pool, user_pool])

def crawl(id, limit=50) :
    '''returns the first 'limit' friends of a user'''
    *not relevant*

Проблемы текущей реализации:

  • Застревает в кликах, которые я уже импортировал, тем самым теряя время и импортирующие потоки простаивают.
  • Добавит больше по мере их указания.

Итак, незначительные улучшения приветствуются, так же как и полное переписывание. Спасибо!

Ответы [ 4 ]

7 голосов
/ 24 августа 2009

Чтобы запомнить идентификаторы пользователей, которых вы уже посетили, вам нужна карта длиной 250 000 целых чисел. Это далеко не "слишком много". Просто сохраняйте такую ​​карту и перемещайтесь только по краям, которые ведут к уже неизвестным пользователям, добавляя их к этой карте в точке нахождения такого края.

Насколько я понимаю, вы близки к реализации поиска в ширину (BFS). Проверьте Google о деталях этого алгоритма. И, конечно же, не забывайте о мьютексах - они вам понадобятся.

2 голосов
/ 24 августа 2009

Нет конкретного алгоритма, который бы помог вам оптимизировать построение графика с нуля. Так или иначе, вам придется посетить каждый узел хотя бы один раз. Делаете ли вы это глубина сначала или ширина сначала не имеет значения с точки зрения скорости. Theran правильно указывает в комментарии ниже, что поиск в ширину, сначала исследуя более близкие узлы, может дать вам более полезный график сразу, до того, как весь граф будет завершен; это может или не может быть проблемой для вас. Он также отмечает, что самая лучшая версия поиска в глубину реализована с использованием рекурсии, которая потенциально может быть проблемой для вас. Обратите внимание, что рекурсия не требуется, однако; Вы можете добавить не полностью исследованные узлы в стек и обрабатывать их линейно, если хотите.

Если вы выполните простую проверку существования новых узлов (O (1), если вы используете хеш для поиска), то циклы вообще не будут проблемой. Циклы являются проблемой, только если вы не храните полный график. Вы можете оптимизировать поиск по графику, но сам этап построения всегда будет занимать линейное время.

Я согласен с другими авторами, что размер вашего графика не должен быть проблемой. 250000 не очень большой!

Относительно одновременного выполнения; график обновляется всеми потоками, поэтому он должен быть синхронизированной структурой данных. Поскольку это Python, вы можете использовать модуль Queue для хранения новых ссылок, которые еще должны обрабатываться вашими потоками.

2 голосов
/ 24 августа 2009

Я действительно не понимаю, почему для добавления узла в БД требуется 10 секунд. Это звучит как проблема. Какую базу данных вы используете? У вас есть строгие ограничения платформы?

С современными системами и их кучей памяти я бы порекомендовал какой-нибудь красивый простой кэш. Вы должны быть в состоянии создать очень быстрый кэш пользовательской информации, который позволит вам избежать повторной работы. Когда вы уже встретили узел, остановите обработку. Это позволит избежать циклов навсегда в кликах.

Если вам нужно разрешить перефразирование существующих узлов через некоторое время, вы можете использовать last_visit_number, который будет глобальным значением в дБ. Если у узла есть этот номер, то этот обходчик - тот, с которым он столкнулся. Если вы хотите автоматически вернуться к каким-либо узлам, вам просто нужно увеличить last_visit_number перед началом сканирования.

По вашему описанию, я не совсем уверен, как вы застряли.

Редактировать ------ Я только заметил, что у вас есть конкретный вопрос. Чтобы увеличить скорость получения новых данных, я бы отслеживал, сколько раз данный пользователь был связан с вашими данными (импортирован или еще не импортирован). При выборе пользователя для сканирования я бы выбрал пользователей с небольшим количеством ссылок. Я бы специально выбрал либо наименьшее количество ссылок, либо случайный выбор среди пользователей с наименьшим количеством ссылок.

Jacob

0 голосов
/ 24 августа 2009

Хотя вы говорите, что получение списка друзей занимает много времени (10 секунд или более), вариант старого доброго алгоритма Дейкстры просто может сработать:

  1. Получить любой узел.
  2. Получить соединение с любого узла, который вы уже загрузили.
  3. Если другой конец еще не загружен, добавьте узел к графику.
  4. Перейти к шагу 2.

Хитрость заключается в том, чтобы правильно выбрать соединение, которое вы загружаете на шаге 2. Несколько коротких замечаний по этому поводу:

  • Вы должны как-то предотвратить загрузку одного и того же соединения дважды или чаще. Выбор случайного соединения и сброс его, если он уже загружен, очень неэффективен, если вы после всех подключений.
  • Если вы хотите в конце концов загрузить все соединения, загрузите все соединения узла одновременно.

Чтобы действительно что-то сказать об эффективности, пожалуйста, предоставьте более подробную информацию о структуре данных.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...