代码之家  ›  专栏  ›  技术社区  ›  Bart Gerard

Oracle SQL中使用递归查询的有向图只访问每个节点一次

  •  6
  • Bart Gerard  · 技术社区  · 7 年前

    在我们的问题域中,我们正在研究一组边,这些边连接在一起形成一个图。 我们必须从左到右、从上到下显示这些链接。

    我们需要在递归期间限制对同一节点的访问,以获得有效的解决方案。

    我们正在寻找解决方案的替代方案,或者限制访问相同边缘的次数。

    非常感谢您的帮助!

    相像的

    Visiting a directed graph as if it were an undirected one, using a recursive query 在postgresql中提供解决方案。 我们需要使用Oracle11g。

    边缘

    A-B, B-D, C-A, C-E, C-F, H-F, E-B, G-D, G-I
    

    图形化

        A
      /   \
    C - E - B - D
      \       /
    H - F   G - I
    

    CREATE TABLE EDGE (
      FROM_ID VARCHAR(10),
      TO_ID   VARCHAR(10)
    );
    
    INSERT INTO EDGE VALUES ('A', 'B');
    INSERT INTO EDGE VALUES ('E', 'B');
    INSERT INTO EDGE VALUES ('C', 'E');
    INSERT INTO EDGE VALUES ('C', 'A');
    INSERT INTO EDGE VALUES ('C', 'F');
    INSERT INTO EDGE VALUES ('B', 'D');
    INSERT INTO EDGE VALUES ('G', 'D');
    INSERT INTO EDGE VALUES ('H', 'F');
    INSERT INTO EDGE VALUES ('G', 'I');
    

    nodes: 'A'
    

    所需输出

    C   A
    C   E
    C   F
    H   F
    A   B
    E   B
    B   D
    G   D
    G   I
    

    SELECT
      c.LVL,
      c.FROM_ID,
      c.TO_ID,
      CASE
      WHEN lag(C.TO_ID)
           OVER (
             PARTITION BY C.LVL
             ORDER BY C.LVL, C.TO_ID ) = C.TO_ID
        THEN C.LVL || '-' || C.TO_ID
      WHEN lead(C.TO_ID)
           OVER (
             PARTITION BY C.LVL
             ORDER BY C.LVL, C.TO_ID ) = C.TO_ID
        THEN C.LVL || '-' || C.TO_ID
      ELSE C.LVL || '-' || C.FROM_ID
      END GROUP_ID
    FROM (
           WITH chain(LVL, FROM_ID, TO_ID ) AS (
             SELECT
               1            LVL,
               root.FROM_ID FROM_ID,
               root.TO_ID   TO_ID
             FROM EDGE root
             WHERE root.TO_ID IN (:nodes)
                   OR (root.FROM_ID IN (:nodes) AND NOT EXISTS(
                 SELECT *
                 FROM EDGE
                 WHERE TO_ID IN (:nodes)
             ))
             UNION ALL
             SELECT
               LVL +
               CASE
               WHEN previous.TO_ID = the_next.FROM_ID
                 THEN 1
               WHEN previous.TO_ID = the_next.TO_ID
                 THEN 0
               WHEN previous.FROM_ID = the_next.FROM_ID
                 THEN 0
               ELSE -1
               END              LVL,
               the_next.FROM_ID FROM_ID,
               the_next.TO_ID   TO_ID
             FROM EDGE the_next
               JOIN chain previous ON previous.TO_ID = the_next.FROM_ID
                                      OR the_next.TO_ID = previous.FROM_ID
                                      OR (previous.TO_ID = the_next.TO_ID AND previous.FROM_ID <> the_next.FROM_ID)
                                      OR (previous.TO_ID <> the_next.TO_ID AND previous.FROM_ID = the_next.FROM_ID)
           )
             SEARCH BREADTH FIRST BY FROM_ID SET ORDER_ID
             CYCLE FROM_ID, TO_ID SET CYCLE TO 1 DEFAULT 0
           SELECT
             C.*,
             row_number()
             OVER (
               PARTITION BY LVL, FROM_ID, TO_ID
               ORDER BY ORDER_ID ) rank
           FROM chain C
           ORDER BY LVL, FROM_ID, TO_ID
         ) C
    WHERE C.rank = 1;
    
    1 回复  |  直到 7 年前
        1
  •  2
  •   peter.hrasko.sk Florin Ghita    7 年前

    为了避免遍历算法返回已访问的边,确实可以将访问的边保留在某个位置。正如您已经发现的那样,使用字符串连接不会取得多大成功。然而,还有其他可用的“值串联”技术。。。

    create or replace type arr_strings is table of varchar2(64);
    

    然后,您可以在每次迭代中将访问的边收集到该集合中:

    with nondirected$ as (
        select from_id, to_id, from_id||'-'||to_id as edge_desc
        from edge
        where from_id != to_id
        union all
        select to_id, from_id, from_id||'-'||to_id as edge_desc
        from edge
        where (to_id, from_id) not in (
                select from_id, to_id
                from edge
            )
    ),
    graph$(lvl, from_id, to_id, edge_desc, visited_edges) as (
        select 1, from_id, to_id, edge_desc,
            arr_strings(edge_desc)
        from nondirected$ R
        where from_id in (&nodes)
        --
        union all
        --
        select
            lvl+1,
            Y.from_id, Y.to_id, Y.edge_desc,
            X.visited_edges multiset union arr_strings(Y.edge_desc)
        from graph$ X
            join nondirected$ Y
                on Y.from_id = X.to_id
        where not exists (
                select 1
                from table(X.visited_edges) Z
                where Y.edge_desc = Z.column_value
            )
    )
    search breadth first by edge_desc set order_id
        cycle edge_desc set is_cycle to 1 default 0,
    ranked_graph$ as (
        select C.*,
            row_number() over (partition by edge_desc order by lvl, order_id) as rank$
        from graph$ C
    --    where is_cycle = 0
    )
    select *
    from ranked_graph$
    --where rank$ <= 1
    order by lvl, order_id
    ;
    

    笔记

    1. 我将有向图预处理为无向图 union
    2. 我记得几年前在Oracle 11.2上尝试过这样的东西。我记得它失败了,尽管我不记得为什么。在12.2版本,它运行正常。只需在11g上试一试;我没有可用的。
    3. 正如您可能从我的评论中理解的那样,您必须自己解决所需的订购问题。:-)

    将重新访问的边限制为零

    请帮忙?

    arr_output l_visited_nodes .你有多种选择,明智地选择。

    create or replace
    package pkg_so_recursive_traversal
    is
    
    
    type rec_output                     is record (
        from_id                             edge.from_id%type,
        to_id                               edge.to_id%type,
        lvl                                 integer
    );
    type arr_output                     is table of rec_output;
    
    
    function traverse_a_graph
        ( i_from                        in arr_strings
        , i_is_directed                 in varchar2 default 'NO' )
        return arr_output
        pipelined;
    
    
    end pkg_so_recursive_traversal;
    /
    create or replace
    package body pkg_so_recursive_traversal
    is
    
    
    function traverse_a_graph
        ( i_from                        in arr_strings
        , i_is_directed                 in varchar2 )
        return arr_output
        pipelined
    is
        l_next_edges                    arr_output;
        l_current_edges                 arr_output;
        l_visited_edges                 arr_output := arr_output();
        l_out                           rec_output;
        i                               pls_integer;
        l_is_directed                   varchar2(32) := case when i_is_directed = 'YES' then 'YES' else 'NO' end;
    begin
        select E.from_id, E.to_id, 0
        bulk collect into l_next_edges
        from table(i_from) F
            join edge E
                on F.column_value in (E.from_id, case when l_is_directed = 'YES' then null else E.to_id end)
        where E.from_id != E.to_id;
    
        l_out.lvl := 0;
    
        loop
            dbms_output.put_line(l_next_edges.count());
            exit when l_next_edges.count() <= 0;
            l_out.lvl := l_out.lvl + 1;
    
            -- spool the edges to output
            i := l_next_edges.first();
            while i is not null loop
                l_out.from_id := l_next_edges(i).from_id;
                l_out.to_id := l_next_edges(i).to_id;
                pipe row(l_out);
                i := l_next_edges.next(i);
            end loop;
    
            l_current_edges := l_next_edges;
            l_visited_edges := l_visited_edges multiset union l_current_edges;
    
            -- find next edges
            select unique E.from_id, E.to_id, 0
            bulk collect into l_next_edges
            from table(l_current_edges) CE
                join edge E
                    on CE.to_id in (E.from_id, case when l_is_directed = 'YES' then null else E.to_id end)
                    or l_is_directed = 'NO' and CE.from_id in (E.from_id, E.to_id)
            where E.from_id != E.to_id
                and not exists (
                    select 1
                    from table(l_visited_edges) VE
                    where VE.from_id = E.from_id
                        and VE.to_id = E.to_id
                );
        end loop;
    
        return;
    end;
    
    
    end pkg_so_recursive_traversal;
    
    /
    

    A 考虑到图是无向的。。。

    select *
    from table(pkg_so_recursive_traversal.traverse_a_graph(
            i_from => arr_strings('A'),
            i_is_directed => 'NO'
        ));
    

    …它产生。。。

    FROM_ID    TO_ID             LVL
    ---------- ---------- ----------
    A          B                   1
    C          A                   1
    C          E                   2
    B          D                   2
    C          F                   2
    E          B                   2
    G          D                   3
    H          F                   3
    G          I                   4
    

    1. 再说一次,我没有做出任何努力来保持你所要求的订单,因为你说这没那么重要。
    2. 这是对 edge 桌子与具有冗余边缘访问的纯SQL解决方案相比,这可能对性能造成更大的影响,也可能不会。适当测试更多解决方案,看看哪一个最适合你。
    3. 这段特殊的代码将适用于12c及更高版本。对于11g及以下,您必须申报 rec_output arr\U输出