【问题标题】:Challenge,how to implement an algorithm for six degree of separation?挑战,如何实现六度分离的算法?
【发布时间】:2018-07-08 18:42:29
【问题描述】:

用户A-用户B-用户C-用户D-用户F

用“-”连接的用户彼此认识。

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

  1. 计算从 UserX 到 UserY 的路径
  2. 对于 UserX,计算距离不超过 3 步的所有用户。

有没有有效的解决方案?

编辑

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

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

再次编辑

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

【问题讨论】:

  • 你需要一种算法来找到一个用户('UserX')的路径/距离,还是你需要它来为所有用户提供它?
  • 什么版本的 SQL?所有具体的实现都与标准有很大的不同。
  • 当我读到“六度分离”时,我在想 MVC 平方。哦,噩梦。
  • 有多少用户,多久更新一次,是否有人被删除?
  • 从距离 = 6 且 PERSON = 'Kevin Bacon' 的人中选择 *

标签: sql performance algorithm


【解决方案1】:

用图表表示这个用户列表

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

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

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

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

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

如果每个人平均有 100 个朋友,这可能需要查看多达大约 2,020,200 个用户,而 Dijkstra 的算法则需要 10,100 亿。实际上,这些数字会小得多,因为您的两个朋友通常也是彼此的朋友。

这是解决六度分离的唯一方法 (到目前为止提到的) 在实践中可行。

【讨论】:

【解决方案2】:

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

  • 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] );

【讨论】:

  • 不知道你的背景很难说
  • Dijkstra 的算法在这种情况下归结为广度优先搜索,对吧?如果你有很多用户,我认为它会很慢。
  • 是的,Dijkstra的算法适用于加权图
  • 要使用 A*,您必须有某种启发式方法,这将非常有趣。
  • 那会很有趣 - 它必须采用数字的形式,表明一个成员认识另一个成员的可能性有多大。根据您可用的数据,这当然是可能的 - Facebook 这样做是为了推荐您可能不认识的朋友是会员。
【解决方案3】:

我有一个与你已有的完全不同的建议。如果您必须坚持使用 SQL 数据库并且您不了解任何 Java,那么这个建议将没有多大用处。

您的问题具体是一个图形问题,所以我建议虽然使用 SQL 数据库来存储图形会起作用,但另一种方法是使用专门针对图形问题的解决方案。

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

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

在他们的 wiki 上使用 Neo4j 的适当示例演示了使用 IMDB 数据的degrees-of-separation web application。该示例说明了任何演员和 Kevin Bacon 之间的最短路径计算。

我喜欢这个例子,因为它谈到了很多关于建模你的图表将代表的领域。为您的域建模可确保您考虑以下事项:

  1. 有向与无向
  2. 边缘类型/关系
  3. 边缘权重等属性

正如在其他帖子中提到的,有许多算法可用于计算最短路径,例如 Dijkstra、Floyd Warshall 或 BFS。所有这些都已在 Neo4j 中实现,并提供了一些示例here

【讨论】:

    【解决方案4】:

    假设源数据在一个表中: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
    

    【讨论】:

      【解决方案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
      

      【讨论】:

        【解决方案6】:

        我不久前看过这个,但无法为 Web 应用程序想出一个有效的解决方案。

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

        请参阅此处了解我的google group post 有一个 SQL 和一个 C# 解决方案。

        注意:您应该在 Google 上搜索“Dijkstra 算法”,因为众所周知,它是一种找到最短路径的好算法。

        编辑:试试这个link

        顺便说一句CLR方法执行得最快。

        【讨论】:

        • 是的,我听说过那个算法。但它似乎不适合这里。因为它只能找到一个最短路径。
        • 为什么不适合?这会找到所有路径,然后过滤最短的路径。它可以很简单地适应您的需求
        • 添加了另一个链接,尽管它对我来说很好用。还在这里添加了groups.google.co.uk/group/…
        • 对不起,它报告“连接已重置..”
        【解决方案7】:

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

        伪代码可以在以下位置找到:

        [维基百科][1]

        dijkstra 和一个用于 DFS 的 python 中:

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

        【讨论】:

          【解决方案8】:

          对于任务 2,您不会比广度优先搜索做得更好,除非可能通过缓存。

          对于任务 1,将您的解决方案应用于任务 2。找到距离用户 X 不超过 3 跳的所有用户。当然,如果用户 Y 在该集合中,那么您就完成了。如果不是,请从用户 Y 开始进行广度优先搜索,并在您到达任何您已经知道可以从 X 到达的用户时停止。

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

          【讨论】:

          • 对于任务 1,您可以变得更聪明一点:将 X 和 Y 放在单独的哈希集中;轮流扩展 X 和 Y 的网络,直到找到他们共同的用户。
          【解决方案9】:

          (这个答案相当于 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进行加法:

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

          【讨论】:

            【解决方案10】:

            实际上有一种相当有效的方法可以使用 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,InnoDB 配置为笔记本电脑硬盘上的 512MB 缓冲池。大约 1600 万次辅助键读取。冷,没有数据预加载到缓冲池中。 2010 款 MacBook Pro。

            当然,如果表可以放在缓冲池中会快很多。

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

            【讨论】:

              【解决方案11】:
              【解决方案12】:

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

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

              选择 Know.P1、Know.P2、Know_1.P2、Know_2.P2

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

              INNER JOIN Know AS Know_2 ON Know_1.P2 = Know_2.P1

              WHERE (((Know_1.P2)[Know].[P1]) AND ((Know_2.P2)[Know].[P1] And (Know_2.P2)[Know].[ P2]))

              按Know.P1、Know.P2、Know_1.P2、Know_2.P2排序;

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

              【讨论】:

                猜你喜欢
                • 2020-04-21
                • 2022-07-24
                • 2013-03-30
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-04-15
                • 1970-01-01
                相关资源
                最近更新 更多