挑战,如何实现六度分离算法?

时间:2010-01-16 08:32:22

标签: sql performance algorithm

用户A-用户B-用户C-UserD-UserF

通过' - '连接的用户互相认识。

我需要一个算法来完成这两项任务:

  1. 计算从UserX到UserY的路径
  2. 对于UserX,计算距离不超过3步的所有用户。
  3. 是否有有效的解决方案?

    修改

    我的目的不是证明它是对还是错,而是在必要时实时计算结果。

    另外,我认为最具表现力的方式是代码,甚至伪代码。

    再次编辑

    我已经决定这种工作必须在数据库内完成,所以它必须是一个sql解决方案!

12 个答案:

答案 0 :(得分:16)

通过图表

表示此用户列表
  • 每个用户都是一个节点
  • 任何两个相互认识的用户之间存在优势
  
      
  1. 计算从UserX到UserY的路径
  2.   
  3. 对于UserX,计算距离不超过3步的所有用户。
  4.   

这些问题密切相关,相同的算法实际上解决了这两个问题。您可以将其命名为“Dijkstra算法,所有边缘都具有权重1,”“广度优先搜索。”

基本上,从第一个节点开始,访问其所有亲戚;然后将它们全部标记为已访问,记录每个最短路径(到达它们的最短路径+刚刚遍历的边缘),并为每个路径重复。到达问题#1的目的地后停止,在最短路径后停止>问题#2为3。

这将在O(n)时间内运行。不,没有更快的方法。

用于六度分离的最快O(n)算法可能会找到距离 UserX UserY一步的所有用户的集合,并找到这两组的交集。如果没有,则从 UserX 添加用户两步并交叉;然后从 UserY 添加用户2步并交叉;最多3个。

如果每个人平均有100个朋友,这可能需要查看大约 2,020,200个用户,而不是Dijkstra算法的 1,010亿。在实践中,这些数字会小得多,因为你的两个朋友通常也是彼此的朋友。

这是解决六度分离的唯一方法 (目前为止提到的那些) 将在实践中发挥作用。 < / p>

答案 1 :(得分:8)

图形算法可以为您提供帮助。了解它们也很有趣!

  • Graph connectivity用于连接。
  • 用户之间路径的Dijkstra(A *)
  • 简单DFS,用于查找远离用户的所有用户N个节点
如果你想找到两个用户之间的最短路径,应该使用Dijkstra或类似的东西。它们之间可能有几条路径,Dijkstra注意到它找到了一条短路径而不是之前发现的路径。

要查找所有用户之间的最短路径,您必须使用Floyd-Warshall之类的内容。这是动态编程的一个很好的例子,实现起来非常简单。来自维基百科的伪代码是:

 1 /* Assume a function edgeCost(i,j) which returns the cost of the edge from i to j
 2    (infinity if there is none).
 3    Also assume that n is the number of vertices and edgeCost(i,i) = 0
 4 */
 5
 6 int path[][];
 7 /* A 2-dimensional matrix. At each step in the algorithm, path[i][j] is the shortest path
 8    from i to j using intermediate vertices (1..k−1).  Each path[i][j] is initialized to
 9    edgeCost(i,j) or infinity if there is no edge between i and j.
10 */
11
12 procedure FloydWarshall ()
13    for k := 1 to n
14       for i := 1 to n
15          for j := 1 to n
16             path[i][j] = min ( path[i][j], path[i][k]+path[k][j] );

答案 2 :(得分:6)

我的建议与您已有的建议完全不同。如果你必须坚持使用SQL数据库并且你不知道任何java,那么这个建议将没什么用处。

你的问题特别是一个图形问题,所以我建议虽然使用SQL数据库来存储图形是可行的,但另一种方法是使用专门用于图形问题的解决方案。

Neo4j项目提供了一个基于磁盘的图形数据库,它将共同提供许多图形算法。引用:

  

Neo4j是一个图形数据库。它是一个   嵌入式,基于磁盘,完全   事务性Java持久性引擎   存储以图形结构化的数据   而不是在表格中。图表   (网络的数学术语)是   允许的灵活数据结构   一种更敏捷,更快速的风格   发展。

在他们的wiki上使用Neo4j的适当示例演示了degrees-of-separation web application使用IMDB数据。这个例子说明了任何演员和凯文·培根之间的最短路径计算。

我喜欢这个例子,因为它谈论了很多关于图形将代表的域的建模。对您的域建模可确保您考虑过以下内容:

  1. 直接对抗无向
  2. 边缘类型/关系
  3. 边缘权重等属性
  4. 正如其他帖子中所提到的,有许多用于计算最短路径的算法,例如Dijkstra,Floyd Warshall或BFS。所有这些都已在Neo4j中实现,并提供了一些示例here

答案 3 :(得分:6)

假设源数据在表中:Connections:(PersonID,KnowsPersonID)

1)这必须采用广度优先的方法。由于问题的指数性,你的良好表现潜力是有限的(尽管这种指数性质是理论上你只需要 6 度:D)的原因。 确保限制搜索深度。无论您选择哪种SQL,您最好使用其迭代扩展而不是基于纯集的解决方案。

以下是使用Microsoft的T-SQL的基本方法:

CREATE PROCEDURE FindPath (@UserX int, @UserY int)

CREATE TABLE #SixDegrees(
  ConnectedUser int,
  Depth int,
  Path varchar(100),
  CONSTRAINT PK_SixDegrees PRIMARY KEY CLUSTERED (
    ConnectedUser
  )
)

DECLARE @Depth int,
        @PathFound varchar(100)
SET @Depth = 0

INSERT INTO #SixDegrees (@UserX, 0, CAST(@UserX as varchar))
/*Next line just in case X & Y are the same*/
SET @PathFound = (SELECT Path 
                  FROM #SixDegrees 
                  WHERE ConnectedUser = @UserY)

WHILE @Depth < 6 AND @PathFound IS NULL
BEGIN
  SET @Depth = @Depth + 1
  INSERT INTO #SixDegrees
  SELECT  k.KnowsPersonID, @Depth, (SELECT Path 
                                    FROM #SixDegrees 
                                    WHERE ConnectedUser = k.Link) + ',' + CAST(k.KnowsPersonID AS varchar)
  FROM (
      SELECT  MIN(ConnectedUser) Link, KnowsPersonID
      FROM    #SixDegrees
              JOIN Connections ON
                PersonID = ConnectedUser
      WHERE   Depth = @Depth
              /*EDIT: Added the following*/
              AND KnowsPersonID NOT IN (
                  SELECT  ConnectedUser
                  FROM    #SixDegrees
                  )
      GROUP BY KnowsPersonID
      ) k

  SET @PathFound = (SELECT Path 
                    FROM #SixDegrees 
                    WHERE ConnectedUser = @UserY)
END

IF @Path IS NULL
  PRINT 'No path found'
ELSE
  PRINT @Path
GO

编辑:在上面的解决方案中,我最初忘记排除已经在#SixDegrees临时表中的用户。

2)对上面的一点调整总是循环到3的深度会让你的#SixDegrees包含你感兴趣的所有用户。

但是,以下基于纯集的解决方案应该更有效:

SELECT  DISTINCT KnowsPersonID
FROM    Connections
WHERE   PersonID IN (
    SELECT  DISTINCT KnowsPersonID
    FROM    Connections
    WHERE   PersonID IN (
        SELECT  KnowsPersonID
        FROM    Connections
        WHERE   PersonID = @UserX
        ) l1
    ) l2

答案 4 :(得分:5)

以下脚本是用sybase sql编写的。您可能需要根据数据库服务器进行微小的修改。

问题1。

create table #connections (
    my_user  varchar(10)  not null  ,
    knows varchar(10)  not null  ,
        CONSTRAINT connection_pk PRIMARY KEY CLUSTERED ( my_user, knows)   
) 

create table #traversed (id varchar(10) primary key)

insert into #connections VALUES ('UserA','UserB')
insert into #connections VALUES ('UserB','UserA')
insert into #connections VALUES ('UserB','UserC')
insert into #connections VALUES ('UserC','UserB')
insert into #connections VALUES ('UserC','UserD')
insert into #connections VALUES ('UserD','UserC')
insert into #connections VALUES ('UserD','UserF')
insert into #connections VALUES ('UserF','UserD')

DECLARE @str_sql   varchar(200)               
DECLARE @str_order varchar(60)

declare @start varchar(10)
set @start = ('UserD')
declare @end varchar(10)
set @end = ('UserA')

if (@start >= @end)
    set @str_order = " order by id desc"
else
    set @str_order = " order by id asc"


INSERT INTO #traversed VALUES (@start)

WHILE (select count(*) from #traversed where id = @end) = 0    
BEGIN     
  INSERT INTO #traversed (id)    
  SELECT DISTINCT knows  
  FROM #connections e JOIN #traversed p ON p.id = e.my_user  
  WHERE e.knows NOT IN (SELECT id FROM #traversed)     
  AND e.knows between (select case when @start < @end then @start else @end end)  
      and (select case when @start < @end then @end  else @start end) 
END

set @str_sql = "SELECT #traversed.id FROM #traversed" + @str_order 
exec (@str_sql)

问题2。

create table #connections (
    my_user  varchar(10)  not null  ,
    knows varchar(10)  not null  ,
        CONSTRAINT connection_pk PRIMARY KEY CLUSTERED ( my_user, knows)   
) 

create table #traversed (id varchar(10) primary key)

insert into #connections VALUES ('UserA','UserB')
insert into #connections VALUES ('UserB','UserA')
insert into #connections VALUES ('UserB','UserC')
insert into #connections VALUES ('UserC','UserB')
insert into #connections VALUES ('UserC','UserD')
insert into #connections VALUES ('UserD','UserC')
insert into #connections VALUES ('UserD','UserF')
insert into #connections VALUES ('UserF','UserD')

declare @start varchar(10)
set @start = ('UserB')

declare @higher_counter int
declare @lower_counter int

set @higher_counter = 0
set @lower_counter = 0

INSERT INTO #traversed VALUES (@start)

WHILE (@higher_counter < 3)
BEGIN     
  INSERT INTO #traversed (id)    
  SELECT DISTINCT knows  
  FROM #connections e JOIN #traversed p ON p.id = e.my_user  
  WHERE e.knows NOT IN (SELECT id FROM #traversed)     
  AND e.knows > @start 

  set @higher_counter = @higher_counter +1
END  

WHILE (@lower_counter < 3)
BEGIN     
  INSERT INTO #traversed (id)    
  SELECT DISTINCT knows  
  FROM #connections e JOIN #traversed p ON p.id = e.my_user  
  WHERE e.knows NOT IN (SELECT id FROM #traversed)     
  AND e.knows < @start 

  set @lower_counter = @lower_counter +1
END   

SELECT #traversed.id FROM #traversed

答案 5 :(得分:2)

我刚才看过这个问题,无法为网络应用程序提供有效的解决方案。

我最终得到了5个级别而不是6个级别

在这里查看我的google group post有一个SQL和一个C#解决方案。

注意:您应该谷歌搜索“Dijkstra算法”,因为它已知是一种很好的算法来找到最短路径。

修改:试试这个link

BTW CLR方法执行得最快。

答案 6 :(得分:2)

第一个问题可以用dijkstra算法解决。 第二个使用DFS算法。 其他人已经说过,只是想指出,两种问题的最有效解决方案在一种算法中都不可用。

伪代码可以在:

找到

[维基百科] [1]

代表dijkstra,另一个代表python代表DFS:

http://en.wikipedia.org/wiki/Depth-first_search

答案 7 :(得分:2)

对于任务2,除了广度优先搜索之外,你不会做得更好。除了可能通过缓存。

对于任务1,将应用程序应用于任务2.查找距离用户X不超过3跳的所有用户。当然,如果用户Y在该集合中,那么您就完成了。如果没有,请从用户Y开始进行广度优先搜索,并在您到达任何已知的用户时立即停止。

(如果你在任务2中缓存了一些关于你如何到达每个用户的信息,那么当你在任务1中找到一个链接时,很容易重建确切的路径。)

答案 8 :(得分:2)

(这个答案相当于Djikstra的。它基本上是一个实现细节。)

要回答#2,您可以使用布尔矩阵乘法来确定程度为P的连通性。

假设你有一个布尔矩阵M,其中:

M(A, B)= A is directly connected to B

然后

(M(A, B))^P= A is connected to B within P links.

矩阵乘法应使用AND进行乘法,使用OR进行加法:

您可以通过仅对先前为假的条目进行乘法以及意识到矩阵是对称的来对此进行大量优化。这是留给读者的练习。

答案 9 :(得分:2)

实际上,使用OQGraph使用MariaDB可以做到这一点。

假设数据包含在两个表中:

CREATE TABLE `entity` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` enum('ACTOR','MOVIE','TV MOVIE','TV MINI','TV SERIES','VIDEO MOVIE','VIDEO GAME','VOICE','ARCHIVE') NOT NULL,
  `name` varchar(128) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `type` (`type`,`name`) USING BTREE
) ENGINE=InnoDB;

CREATE TABLE `link` (
  `rel_id` int(11) NOT NULL AUTO_INCREMENT,
  `link_from` int(11) NOT NULL,
  `link_to` int(11) NOT NULL,
  PRIMARY KEY (`rel_id`),
  KEY `link_from` (`link_from`,`link_to`),
  KEY `link_to` (`link_to`)
) ENGINE=InnoDB;

然后可以将OQGraph虚拟表声明为:

CREATE TABLE movie_graph (
  latch SMALLINT UNSIGNED NULL,
  origid BIGINT UNSIGNED NULL,
  destid BIGINT UNSIGNED NULL,
  weight DOUBLE NULL,
  seq BIGINT UNSIGNED NULL,
  linkid BIGINT UNSIGNED NULL,
  KEY (latch, origid, destid) USING HASH,
  KEY (latch, destid, origid) USING HASH
) ENGINE=OQGRAPH 
  data_table='link' origid='link_from' destid='link_to';

然后可以查询数据:

MariaDB [imdb]> SELECT
             -> GROUP_CONCAT(name ORDER BY seq SEPARATOR ' -> ') AS path
             -> FROM imdb_graph JOIN entity ON (id=linkid)
             -> WHERE latch=1
             -> AND origid=(SELECT a.id FROM entity a
             ->             WHERE name='Kevin Bacon')
             -> AND destid=(SELECT b.id FROM entity b
                            WHERE name='N!xau')\G
*************************** 1. row ***************************
path: Kevin Bacon -> The 45th Annual Golden Globe Awards (1988) -> Richard Attenborough -> In Darkest Hollywood: Cinema and Apartheid (1993) -> N!xau
1 row in set (10 min 6.55 sec)

大约370万个节点和3000万个边的图形。表大约是3.5GB,笔记本电脑硬盘上为512MB缓冲池配置了InnoDB。大约有1600万次辅助密钥读取。冷,没有数据预加载到缓冲池中。 2010 MacBook Pro。

当然,如果表可以保存在缓冲池中,则速度要快得多。

此示例来自:https://www.slideshare.net/AntonyTCurtis/oqgraph-scale2013-17332168/21

答案 10 :(得分:1)

答案 11 :(得分:1)

我只响应SQL解决方案。这为所有路径提供了3步之遥,尽管对于大型数据集可能不是“高效”。表“KNOW”,“KNOW_1”等都是相同的,并且具有两个字段P1和P2。只有当1)P1知道P2或2)P2知道P1时才有条目。 P1和P2中的数据可以是对应于每个人的任意字符串。

这个Acccess SQL查询应该产生知道b知道c知道没有周期的所有路径(例如,知道b知道c知道a)。您仍然需要消除重复项(abcd = dcba),但应该能够在第二步中轻松完成。通过防止Where语句中的先前人员的重复来消除循环。

SELECT Know.P1,Know.P2,Know_1.P2,Know_2.P2

FROM(了解INNER JOIN了解AS Know_1 ON Know.P2 = Know_1.P1)

INNER JOIN了解AS Know_2 ON Know_1.P2 = Know_2.P1

WHERE(((Know_1.P2)&lt;&gt; [Know]。[P1])AND((Know_2.P2)&lt;&gt; [Know]。[P1] And(Know_2.P2)&lt;&gt; ; [专有] [P2]))

通过Know.P1,Know.P2,Know_1.P2,Know_2.P2订购;

不像以前的解决方案那么优雅,但似乎工作正常。我们在使用约束编程进行类似工作方面有一些经验,并发现SQL进程对于某些问题更快。