好的图遍历算法
Posted
技术标签:
【中文标题】好的图遍历算法【英文标题】:Good graph traversal algorithm 【发布时间】:2009-08-24 06:05:32 【问题描述】:抽象问题:我有一个大约 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*
当前实施的问题:
卡在我已经导入的派系中,从而浪费时间并且导入线程处于空闲状态。 会在有人指出时添加更多内容。因此,欢迎进行边际改进以及完全重写。谢谢!
【问题讨论】:
与几个著名的图论 (!) 算法的发现者 Robert Tarjan 有任何关系吗? :) 可悲的是,只有匈牙利的小镇是我们俩的姓氏。但我们都热爱计算机和数学。 与问题无关,但请注意 sys.stderr.write("...\n") 可以替换为 print >> sys.stderr, "..." (不需要“\n”,并使用更常见的打印语句)。 对,但是我正在将标准输出重定向到一个文件(你不可能知道),并且仍然希望错误消息显示在我的控制台中。 【参考方案1】:要记住您已经访问过的用户的 ID,您需要一个长度为 250,000 个整数的地图。这远非“太多”。只需维护这样一个地图,并且只遍历通向已经未被发现的用户的边,在找到这样的边时将他们添加到该地图中。
据我所知,您已经接近实现广度优先搜索 (BFS)。检查谷歌有关此算法的详细信息。当然,不要忘记互斥体——你会需要它们。
【讨论】:
用户实际上是平均长度为 15 的字符串。我尝试使用带有 username1 : True, username2 : True 的字典,但很快就达到了 100% 的内存并且机器锁定了。也许在 python 中使用 dict 效率很低? 一种可能的解决方案是存储用户名的哈希 另外,set 比 dict 更适合这种类型的存储 在文档 (docs.python.org/library/stdtypes.html#set-types-set-frozenset) 中说,“设置元素,如字典键,必须是可散列的”,所以我假设是这样 而且查找也很好:-) blog.tplus1.com/index.php/2008/03/17/…【参考方案2】:我真的很困惑为什么将节点添加到数据库需要 10 秒。这听起来像是个问题。你用的是什么数据库?您有严格的平台限制吗?
对于现代系统及其大量内存,我建议使用某种不错的简单缓存。您应该能够创建一个非常快速的用户信息缓存,以避免重复工作。当您已经遇到一个节点时,停止处理。这将避免在派系中永远骑自行车。
如果您需要允许在一段时间后重新散列现有节点,您可以使用 last_visit_number,它是一个以 dB 为单位的全局值。如果该节点具有该编号,则此爬网就是遇到它的那个。如果你想自动重新访问任何节点,你只需要在开始爬取之前碰撞 last_visit_number 。
根据你的描述,我不太确定你是怎么卡住的。
编辑 ------ 我刚刚注意到你有一个具体的问题。为了提高您提取新数据的速度,我会跟踪给定用户在您的数据中链接到的次数(已导入或尚未导入)。在选择要抓取的用户时,我会选择链接数量较少的用户。我会专门选择最少的链接数或在链接数最少的用户中随机选择。
雅各布
【讨论】:
这 10 秒来自于必须抓取几页有关用户的信息,然后将其转换为我的数据库格式。大部分是网络时间。 至于新用户的选择,很有意思。我将尝试计算用户的链接,并且只从低链接用户中爬取。 为什么线程这么少?你担心他们会阻止你吗?我打算为每个节点建议一个哈希(a.la Pavel)。您可以做的一件事是创建一个递增的 id 并使用一个简单的映射表来交叉引用它们。 是的,我担心远程站点会阻止我。礼貌和所有这些(在雅虎!我知道我们的爬虫随时针对 2 个请求/主机)。随着内存集的工作,我不需要优化我的爬网路径,但如果其他站点按数量级增长,我将不得不转向你的策略。谢谢! 一个稍微有点扭曲的选项是通过其 API 来利用搜索引擎的结果来获取您想要的数据。如果您正在构建初始数据库,缓存页面(如果可用)可能会为您提供所需的数据。搜索引擎以极快的速度运行,可能不会介意多线程。【参考方案3】:没有特定的算法可以帮助您从头开始优化图形的构建。无论哪种方式,您都必须至少访问每个节点一次。从速度的角度来看,您是否这样做depth first 或breadth first 是无关紧要的。 Theran 在下面的评论中正确地指出,广度优先搜索,通过首先探索更接近的节点,可能会在整个图完成之前立即为您提供更有用的图;这可能对您来说是一个问题,也可能不是。他还指出,深度优先搜索的最简洁版本是使用递归实现的,这可能会给您带来问题。但是请注意,不需要递归;如果您愿意,您可以将未完全探索的节点添加到堆栈并线性处理它们。
如果您对新节点进行简单的存在性检查(如果您使用散列进行查找,则为 O(1)),那么循环根本不会成为问题。仅当您不存储完整图形时,才需要考虑循环。您可以通过图表优化搜索,但构建步骤本身总是需要线性时间。
我同意其他发帖人的观点,即您的图表大小应该不是问题。 250,000不是很大!
关于并发执行;该图是由所有线程更新的,所以它需要是一个同步的数据结构。由于这是 Python,您可以使用 Queue 模块来存储仍由您的线程处理的新链接。
【讨论】:
BFS 可能会更好,因为它会首先查看离初始节点最近的节点,这可能会在早期提供一个有用的子集。 BFS 还避免了递归 250,000 级深度的风险,并且可以将其队列保持在与最终图相同的数据库中(假设是 RDBMS)。 您当然可以在没有深度堆栈跟踪问题的情况下制作 DFS:DFS 和 BFS 之间唯一真正的区别在于 BFS 您将节点添加到队列中;在 DFS 中,一个堆栈。相同的算法,不同的数据结构——因此,不同的语义。 @Theran, Michael:+1 谢谢 - 调整答案以澄清这一点。 听起来像一个简单的 BFS(就像我一样)是有序的。我 100% 同意你,因为 250,000 很小,但我最初的实现很快就用完了记忆,做一个 username1 : True, username2 : True 的字典。也许我应该把它作为另一个问题发布。【参考方案4】:虽然您说获取好友列表需要很长时间(10 秒或更长时间),但古老的 Dijkstra 算法的变体可能会起作用:
-
获取任意节点。
从您已加载的任何节点获取连接。
如果尚未加载另一端,则将节点添加到图中。
转到第 2 步。
诀窍是巧妙地选择您在第 2 步中加载的连接。关于此的一些简短评论:
您应该以某种方式防止同一连接被加载两次或更多次。如果您毕竟是所有连接,则选择一个随机连接并在它已经加载的情况下将其丢弃是非常低效的。 如果要最终加载所有连接,请同时加载一个节点的所有连接。为了真正说明效率,请提供有关数据结构的更多详细信息。
【讨论】:
以上是关于好的图遍历算法的主要内容,如果未能解决你的问题,请参考以下文章