Thuật toán đồ thị để tìm tất cả các kết nối giữa hai đỉnh tùy ý


117

Tôi đang cố gắng xác định thuật toán hiệu quả về thời gian tốt nhất để hoàn thành nhiệm vụ được mô tả bên dưới.

Tôi có một bộ hồ sơ. Đối với tập hợp các bản ghi này, tôi có dữ liệu kết nối cho biết cách các cặp bản ghi từ tập hợp này kết nối với nhau. Về cơ bản, điều này đại diện cho một đồ thị vô hướng, với các bản ghi là các đỉnh và dữ liệu kết nối là các cạnh.

Tất cả các bản ghi trong tập hợp đều có thông tin kết nối (nghĩa là không có bản ghi mồ côi nào có mặt; mỗi bản ghi trong tập hợp kết nối với một hoặc nhiều bản ghi khác trong tập hợp).

Tôi muốn chọn hai bản ghi bất kỳ từ tập hợp và có thể hiển thị tất cả các đường dẫn đơn giản giữa các bản ghi đã chọn. Bởi "đường dẫn đơn giản", ý tôi là các đường dẫn không có các bản ghi lặp lại trong đường dẫn (tức là chỉ có các đường dẫn hữu hạn).

Lưu ý: Hai bản ghi được chọn sẽ luôn khác nhau (nghĩa là đỉnh đầu và cuối sẽ không bao giờ giống nhau; không có chu kỳ).

Ví dụ:

    Nếu tôi có các bản ghi sau:
        A, B, C, D, E

    và phần sau đại diện cho các kết nối: 
        (A, B), (A, C), (B, A), (B, D), (B, E), (B, F), (C, A), (C, E),
        (C, F), (D, B), (E, C), (E, F), (F, B), (F, C), (F, E)

        [trong đó (A, B) có nghĩa là bản ghi A kết nối với bản ghi B]

Nếu tôi chọn B làm bản ghi bắt đầu và E làm bản ghi kết thúc, tôi sẽ muốn tìm tất cả các đường dẫn đơn giản thông qua các kết nối bản ghi sẽ kết nối bản ghi B với bản ghi E.

   Tất cả các đường nối từ B đến E:
      B-> E
      B-> F-> E
      B-> F-> C-> E
      B-> A-> C-> E
      B-> A-> C-> F-> E

Đây là một ví dụ, trong thực tế, tôi có thể có các bộ chứa hàng trăm nghìn bản ghi.


Các kết nối được gọi là chu kỳ , và câu trả lời này có rất nhiều thông tin cho bạn.
elhoim

3
Vui lòng cho biết bạn muốn một danh sách hữu hạn các kết nối không có vòng lặp hay luồng kết nối vô hạn với tất cả các vòng lặp có thể có. Cf Câu trả lời của Blorgbeard.
Charles Stewart

Bất cứ ai có thể giúp đỡ với điều này ??? stackoverflow.com/questions/32516706/…
tejas3006

Câu trả lời:


116

Có vẻ như điều này có thể được thực hiện bằng cách tìm kiếm sơ qua trên biểu đồ. Tìm kiếm theo chiều sâu sẽ tìm tất cả các đường đi không theo chu kỳ giữa hai nút. Thuật toán này sẽ rất nhanh và quy mô đến các đồ thị lớn (Cấu trúc dữ liệu đồ thị thưa thớt nên nó chỉ sử dụng nhiều bộ nhớ nhất có thể).

Tôi nhận thấy rằng biểu đồ bạn chỉ định ở trên chỉ có một cạnh là hướng (B, E). Đây là lỗi đánh máy hay thực sự là một biểu đồ có hướng? Giải pháp này hoạt động bất kể. Xin lỗi vì tôi không làm được bằng C, tôi hơi yếu về lĩnh vực đó. Tôi hy vọng rằng bạn sẽ có thể dịch mã Java này mà không gặp quá nhiều khó khăn.

Graph.java:

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

public class Graph {
    private Map<String, LinkedHashSet<String>> map = new HashMap();

    public void addEdge(String node1, String node2) {
        LinkedHashSet<String> adjacent = map.get(node1);
        if(adjacent==null) {
            adjacent = new LinkedHashSet();
            map.put(node1, adjacent);
        }
        adjacent.add(node2);
    }

    public void addTwoWayVertex(String node1, String node2) {
        addEdge(node1, node2);
        addEdge(node2, node1);
    }

    public boolean isConnected(String node1, String node2) {
        Set adjacent = map.get(node1);
        if(adjacent==null) {
            return false;
        }
        return adjacent.contains(node2);
    }

    public LinkedList<String> adjacentNodes(String last) {
        LinkedHashSet<String> adjacent = map.get(last);
        if(adjacent==null) {
            return new LinkedList();
        }
        return new LinkedList<String>(adjacent);
    }
}

Search.java:

import java.util.LinkedList;

public class Search {

    private static final String START = "B";
    private static final String END = "E";

    public static void main(String[] args) {
        // this graph is directional
        Graph graph = new Graph();
        graph.addEdge("A", "B");
        graph.addEdge("A", "C");
        graph.addEdge("B", "A");
        graph.addEdge("B", "D");
        graph.addEdge("B", "E"); // this is the only one-way connection
        graph.addEdge("B", "F");
        graph.addEdge("C", "A");
        graph.addEdge("C", "E");
        graph.addEdge("C", "F");
        graph.addEdge("D", "B");
        graph.addEdge("E", "C");
        graph.addEdge("E", "F");
        graph.addEdge("F", "B");
        graph.addEdge("F", "C");
        graph.addEdge("F", "E");
        LinkedList<String> visited = new LinkedList();
        visited.add(START);
        new Search().depthFirst(graph, visited);
    }

    private void depthFirst(Graph graph, LinkedList<String> visited) {
        LinkedList<String> nodes = graph.adjacentNodes(visited.getLast());
        // examine adjacent nodes
        for (String node : nodes) {
            if (visited.contains(node)) {
                continue;
            }
            if (node.equals(END)) {
                visited.add(node);
                printPath(visited);
                visited.removeLast();
                break;
            }
        }
        for (String node : nodes) {
            if (visited.contains(node) || node.equals(END)) {
                continue;
            }
            visited.addLast(node);
            depthFirst(graph, visited);
            visited.removeLast();
        }
    }

    private void printPath(LinkedList<String> visited) {
        for (String node : visited) {
            System.out.print(node);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Đầu ra chương trình:

B E 
B A C E 
B A C F E 
B F E 
B F C E 

5
Xin lưu ý rằng đây không phải là truyền tải đầu tiên theo chiều rộng. Với chiều rộng, trước tiên bạn truy cập tất cả các nút có khoảng cách 0 đến gốc, sau đó đến những nút có khoảng cách 1, rồi đến 2, v.v.
mweerden 12/09/08

14
Chính xác, đây là một DFS. Một BFS sẽ cần sử dụng một hàng đợi, xếp hàng các nút cấp- (N + 1) để được xử lý sau tất cả các nút cấp N. Tuy nhiên, đối với mục đích của OP, BFS hoặc DFS sẽ hoạt động, vì không có thứ tự sắp xếp ưu tiên của các đường dẫn được chỉ định.
Matt J

1
Casey, tôi đã tìm kiếm giải pháp cho vấn đề này trong nhiều năm. Gần đây tôi đã triển khai DFS này trong C ++ và nó hoạt động tốt.
AndyUK

6
Bất lợi của đệ quy là nếu bạn có đồ thị sâu (A-> B-> C -> ...-> N), bạn có thể có StackOverflowError trong java.
Rrr

1
Tôi đã thêm một phiên bản lặp lại trong C # bên dưới.
batta

23

Từ điển trực tuyến của Viện Tiêu chuẩn và Công nghệ Quốc gia (NIST) về Thuật toán và Cấu trúc Dữ liệu liệt kê vấn đề này là " tất cả các đường dẫn đơn giản" và khuyến nghị tìm kiếm theo chiều sâu . CLRS cung cấp các thuật toán liên quan.

Một kỹ thuật thông minh sử dụng Petri Nets được tìm thấy ở đây


2
Bạn có thể giúp tôi với một giải pháp tốt hơn? DFS mất vĩnh viễn để chạy: stackoverflow.com/q/8342101/632951
Pacerier

Lưu ý rằng thật dễ dàng để đưa ra các đồ thị mà DFS rất kém hiệu quả, mặc dù tập hợp tất cả các đường dẫn đơn giản giữa hai nút là nhỏ và dễ tìm. Ví dụ, hãy xem xét một đồ thị vô hướng trong đó nút bắt đầu A có hai lân cận: nút mục tiêu B (không có lân cận nào ngoài A) và một nút C là một phần của nhóm được kết nối đầy đủ gồm n + 1 nút. Mặc dù rõ ràng chỉ có một con đường đơn giản từ A đến B, một DFS ngây thơ sẽ lãng phí O ( n !) Thời gian vô ích để khám phá bè phái. Các ví dụ tương tự (một giải pháp, DFS mất thời gian theo cấp số nhân) cũng có thể được tìm thấy trong số các DAG.
Ilmari Karonen

NIST nói: "Các đường dẫn có thể được liệt kê bằng cách tìm kiếm theo chiều sâu."
chomp

13

Đây là mã giả mà tôi nghĩ ra. Đây không phải là bất kỳ phương ngữ mã giả cụ thể nào, nhưng phải đủ đơn giản để làm theo.

Bất cứ ai cũng muốn tách cái này ra.

  • [p] là danh sách các đỉnh biểu diễn đường đi hiện tại.

  • [x] là danh sách các đường dẫn đáp ứng các tiêu chí

  • [s] là đỉnh nguồn

  • [d] là đỉnh đích

  • [c] là đỉnh hiện tại (đối số của thói quen PathFind)

Giả sử có một cách hiệu quả để tra cứu các đỉnh liền kề (dòng 6).

     1 PathList [p]
     2 ListOfPathLists [x]
     3 Đỉnh [s], [d]

     4 PathFind (Vertex [c])
     5 Thêm [c] vào cuối danh sách [p]
     6 Đối với mỗi Đỉnh [v] liền kề với [c]
     7 Nếu [v] bằng [d] thì
     8 Lưu danh sách [p] vào [x]
     9 Khác Nếu [v] không có trong danh sách [p]
    10 PathFind ([v])
    11 Tiếp theo Cho
    12 Xóa đuôi khỏi [p]
    13 Trở lại

Bạn có thể vui lòng làm sáng tỏ bước 11 và bước 12 không
người dùng bozo

Dòng 11 chỉ biểu thị khối kết thúc đi với vòng lặp For bắt đầu trên dòng 6. Dòng 12 có nghĩa là loại bỏ phần tử cuối cùng của danh sách đường dẫn trước khi quay trở lại trình gọi.
Robert Groves

Lệnh gọi ban đầu tới PathFind là gì - bạn có chuyển vào [s] đỉnh nguồn không?
người dùng bozo

Trong ví dụ này là có, nhưng hãy nhớ rằng bạn có thể không muốn viết mã thực ánh xạ 1-1 với mã giả này. Nó có ý nghĩa nhiều hơn để minh họa một quá trình suy nghĩ hơn là mã được thiết kế tốt.
Robert Groves

8

Vì triển khai DFS không đệ quy hiện có được đưa ra trong câu trả lời này dường như bị hỏng, hãy để tôi cung cấp một cách thực sự hoạt động.

Tôi đã viết điều này bằng Python, vì tôi thấy nó khá dễ đọc và không gọn gàng bởi các chi tiết triển khai (và bởi vì nó có yieldtừ khóa tiện dụng để triển khai trình tạo ), nhưng nó sẽ khá dễ dàng để chuyển sang các ngôn ngữ khác.

# a generator function to find all simple paths between two nodes in a
# graph, represented as a dictionary that maps nodes to their neighbors
def find_simple_paths(graph, start, end):
    visited = set()
    visited.add(start)

    nodestack = list()
    indexstack = list()
    current = start
    i = 0

    while True:
        # get a list of the neighbors of the current node
        neighbors = graph[current]

        # find the next unvisited neighbor of this node, if any
        while i < len(neighbors) and neighbors[i] in visited: i += 1

        if i >= len(neighbors):
            # we've reached the last neighbor of this node, backtrack
            visited.remove(current)
            if len(nodestack) < 1: break  # can't backtrack, stop!
            current = nodestack.pop()
            i = indexstack.pop()
        elif neighbors[i] == end:
            # yay, we found the target node! let the caller process the path
            yield nodestack + [current, end]
            i += 1
        else:
            # push current node and index onto stacks, switch to neighbor
            nodestack.append(current)
            indexstack.append(i+1)
            visited.add(neighbors[i])
            current = neighbors[i]
            i = 0

Đoạn mã này duy trì hai ngăn xếp song song: một ngăn chứa các nút trước đó trong đường dẫn hiện tại và một chứa chỉ mục hàng xóm hiện tại cho mỗi nút trong ngăn xếp nút (để chúng ta có thể tiếp tục lặp lại các nút lân cận của nút khi chúng ta bật lại nó ngăn xếp). Tôi có thể đã sử dụng tốt như nhau một ngăn xếp các cặp (nút, chỉ mục), nhưng tôi nghĩ rằng phương pháp hai ngăn xếp sẽ dễ đọc hơn và có lẽ dễ triển khai hơn đối với người dùng các ngôn ngữ khác.

Mã này cũng sử dụng một visitedtập hợp riêng biệt , luôn chứa nút hiện tại và bất kỳ nút nào trên ngăn xếp, để cho phép tôi kiểm tra hiệu quả xem một nút đã là một phần của đường dẫn hiện tại hay chưa. Nếu ngôn ngữ của bạn tình cờ có cấu trúc dữ liệu "tập hợp có thứ tự" cung cấp cả hoạt động đẩy / bật lên giống ngăn xếp truy vấn thành viên hiệu quả, bạn có thể sử dụng cấu trúc đó cho ngăn xếp nút và loại bỏ visitedtập hợp riêng biệt .

Ngoài ra, nếu bạn đang sử dụng một lớp / cấu trúc có thể thay đổi tùy chỉnh cho các nút của mình, bạn chỉ có thể lưu trữ cờ boolean trong mỗi nút để cho biết liệu nó đã được truy cập như một phần của đường dẫn tìm kiếm hiện tại hay chưa. Tất nhiên, phương pháp này sẽ không cho phép bạn chạy song song hai tìm kiếm trên cùng một biểu đồ, nếu vì lý do nào đó bạn muốn làm điều đó.

Dưới đây là một số mã kiểm tra chứng minh cách hoạt động của hàm được cung cấp ở trên:

# test graph:
#     ,---B---.
#     A   |   D
#     `---C---'
graph = {
    "A": ("B", "C"),
    "B": ("A", "C", "D"),
    "C": ("A", "B", "D"),
    "D": ("B", "C"),
}

# find paths from A to D
for path in find_simple_paths(graph, "A", "D"): print " -> ".join(path)

Chạy mã này trên biểu đồ ví dụ đã cho sẽ tạo ra kết quả sau:

A -> B -> C -> D
A -> B -> D
A -> C -> B -> D
A -> C -> D

Lưu ý rằng, trong khi đồ thị ví dụ này là vô hướng (nghĩa là tất cả các cạnh của nó đi theo cả hai chiều), thuật toán cũng hoạt động đối với đồ thị có hướng tùy ý. Ví dụ: loại bỏ C -> Bcạnh (bằng cách xóa Bkhỏi danh sách hàng xóm của C) tạo ra cùng một đầu ra ngoại trừ đường dẫn thứ ba ( A -> C -> B -> D), điều này không còn khả thi nữa.


Ps. Thật dễ dàng để xây dựng các biểu đồ mà các thuật toán tìm kiếm đơn giản như thuật toán này (và các thuật toán khác được đưa ra trong chủ đề này) hoạt động rất kém.

Ví dụ: hãy xem xét nhiệm vụ tìm tất cả các đường đi từ A đến B trên một đồ thị vô hướng trong đó nút bắt đầu A có hai lân cận: nút đích B (không có lân cận nào khác ngoài A) và nút C là một phần của một nhóm trong số n nút +1, như thế này:

graph = {
    "A": ("B", "C"),
    "B": ("A"),
    "C": ("A", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "D": ("C", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "E": ("C", "D", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "F": ("C", "D", "E", "G", "H", "I", "J", "K", "L", "M", "N", "O"),
    "G": ("C", "D", "E", "F", "H", "I", "J", "K", "L", "M", "N", "O"),
    "H": ("C", "D", "E", "F", "G", "I", "J", "K", "L", "M", "N", "O"),
    "I": ("C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "O"),
    "J": ("C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "O"),
    "K": ("C", "D", "E", "F", "G", "H", "I", "J", "L", "M", "N", "O"),
    "L": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "M", "N", "O"),
    "M": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "N", "O"),
    "N": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "O"),
    "O": ("C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"),
}

Dễ dàng nhận thấy rằng con đường duy nhất giữa A và B là con đường trực tiếp, nhưng một DFS ngây thơ bắt đầu từ nút A sẽ lãng phí O ( n !) Thời gian vô ích để khám phá các con đường bên trong bè phái, mặc dù rõ ràng là (đối với con người) không có con đường nào có thể dẫn đến B.

Người ta cũng có thể xây dựng các DAG với các thuộc tính tương tự, ví dụ bằng cách để nút bắt đầu A kết nối nút đích B và với hai nút khác C 1 và C 2 , cả hai đều kết nối với nút D 1 và D 2 , cả hai đều kết nối với E 1 và E 2 , v.v. Đối với n lớp nút được sắp xếp như thế này, một tìm kiếm ngây thơ cho tất cả các đường đi từ A đến B sẽ làm lãng phí O (2 n ) thời gian để kiểm tra tất cả các ngõ cụt có thể có trước khi từ bỏ.

Tất nhiên, việc thêm một cạnh vào nút đích B từ một trong những nút trong nhóm (không phải C) hoặc từ lớp cuối cùng của DAG, sẽ tạo ra một số lượng lớn các đường đi có thể từ A đến B theo cấp số nhân và thuật toán tìm kiếm cục bộ thuần túy thực sự không thể nói trước liệu nó có tìm thấy một cạnh như vậy hay không. Do đó, theo một nghĩa nào đó, độ nhạy đầu ra kém của các tìm kiếm ngây thơ như vậy là do họ thiếu nhận thức về cấu trúc toàn cục của biểu đồ.

Mặc dù có nhiều phương pháp tiền xử lý khác nhau (chẳng hạn như loại bỏ lặp đi lặp lại các nút lá, tìm kiếm các dấu phân tách đỉnh đơn nút, v.v.) có thể được sử dụng để tránh một số "kết thúc theo thời gian theo cấp số nhân", tôi không biết bất kỳ điều gì chung thủ thuật tiền xử lý có thể loại bỏ chúng trong mọi trường hợp. Một giải pháp chung là kiểm tra ở mọi bước tìm kiếm xem nút đích có còn có thể truy cập được hay không (sử dụng tìm kiếm phụ) và quay lại sớm nếu không - nhưng than ôi, điều đó sẽ làm chậm tìm kiếm đáng kể (tệ nhất là , tỷ lệ với kích thước của biểu đồ) đối với nhiều biểu đồ không chứa các đường cụt bệnh lý như vậy.


1
Đó là những gì tôi đang tìm kiếm, Cảm ơn bạn :)
arslan

Cảm ơn bạn vì giải pháp không đệ quy DFS của bạn. Chỉ cần lưu ý dòng cuối cùng in kết quả có một lỗi cú pháp, đáng ra for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path)), printthiếu dấu ngoặc đơn.
David Oliván Ubieto

1
@ DavidOlivánUbieto: Đó là mã Python 2, đó là lý do tại sao không có dấu ngoặc đơn. :)
Ilmari Karonen

5

Đây là một phiên bản đệ quy đẹp hơn về mặt logic so với phiên bản thứ hai.

public class Search {

private static final String START = "B";
private static final String END = "E";

public static void main(String[] args) {
    // this graph is directional
    Graph graph = new Graph();
    graph.addEdge("A", "B");
    graph.addEdge("A", "C");
    graph.addEdge("B", "A");
    graph.addEdge("B", "D");
    graph.addEdge("B", "E"); // this is the only one-way connection
    graph.addEdge("B", "F");
    graph.addEdge("C", "A");
    graph.addEdge("C", "E");
    graph.addEdge("C", "F");
    graph.addEdge("D", "B");
    graph.addEdge("E", "C");
    graph.addEdge("E", "F");
    graph.addEdge("F", "B");
    graph.addEdge("F", "C");
    graph.addEdge("F", "E");
    List<ArrayList<String>> paths = new ArrayList<ArrayList<String>>();
    String currentNode = START;
    List<String> visited = new ArrayList<String>();
    visited.add(START);
    new Search().findAllPaths(graph, seen, paths, currentNode);
    for(ArrayList<String> path : paths){
        for (String node : path) {
            System.out.print(node);
            System.out.print(" ");
        }
        System.out.println();
    }   
}

private void findAllPaths(Graph graph, List<String> visited, List<ArrayList<String>> paths, String currentNode) {        
    if (currentNode.equals(END)) { 
        paths.add(new ArrayList(Arrays.asList(visited.toArray())));
        return;
    }
    else {
        LinkedList<String> nodes = graph.adjacentNodes(currentNode);    
        for (String node : nodes) {
            if (visited.contains(node)) {
                continue;
            } 
            List<String> temp = new ArrayList<String>();
            temp.addAll(visited);
            temp.add(node);          
            findAllPaths(graph, temp, paths, node);
        }
    }
}
}

Đầu ra chương trình

B A C E 

B A C F E 

B E

B F C E

B F E 

4

Giải pháp trong mã C. Nó dựa trên DFS sử dụng bộ nhớ tối thiểu.

#include <stdio.h>
#include <stdbool.h>

#define maxN    20  

struct  nodeLink
{

    char node1;
    char node2;

};

struct  stack
{   
    int sp;
    char    node[maxN];
};   

void    initStk(stk)
struct  stack   *stk;
{
    int i;
    for (i = 0; i < maxN; i++)
        stk->node[i] = ' ';
    stk->sp = -1;   
}

void    pushIn(stk, node)
struct  stack   *stk;
char    node;
{

    stk->sp++;
    stk->node[stk->sp] = node;

}    

void    popOutAll(stk)
struct  stack   *stk;
{

    char    node;
    int i, stkN = stk->sp;

    for (i = 0; i <= stkN; i++)
    {
        node = stk->node[i];
        if (i == 0)
            printf("src node : %c", node);
        else if (i == stkN)
            printf(" => %c : dst node.\n", node);
        else
            printf(" => %c ", node);
    }

}


/* Test whether the node already exists in the stack    */
bool    InStack(stk, InterN)
struct  stack   *stk;
char    InterN;
{

    int i, stkN = stk->sp;  /* 0-based  */
    bool    rtn = false;    

    for (i = 0; i <= stkN; i++)
    {
        if (stk->node[i] == InterN)
        {
            rtn = true;
            break;
        }
    }

    return     rtn;

}

char    otherNode(targetNode, lnkNode)
char    targetNode;
struct  nodeLink    *lnkNode;
{

    return  (lnkNode->node1 == targetNode) ? lnkNode->node2 : lnkNode->node1;

}

int entries = 8;
struct  nodeLink    topo[maxN]    =       
    {
        {'b', 'a'}, 
        {'b', 'e'}, 
        {'b', 'd'}, 
        {'f', 'b'}, 
        {'a', 'c'},
        {'c', 'f'}, 
        {'c', 'e'},
        {'f', 'e'},               
    };

char    srcNode = 'b', dstN = 'e';      

int reachTime;  

void    InterNode(interN, stk)
char    interN;
struct  stack   *stk;
{

    char    otherInterN;
    int i, numInterN = 0;
    static  int entryTime   =   0;

    entryTime++;

    for (i = 0; i < entries; i++)
    {

        if (topo[i].node1 != interN  && topo[i].node2 != interN) 
        {
            continue;   
        }

        otherInterN = otherNode(interN, &topo[i]);

        numInterN++;

        if (otherInterN == stk->node[stk->sp - 1])
        {
            continue;   
        }

        /*  Loop avoidance: abandon the route   */
        if (InStack(stk, otherInterN) == true)
        {
            continue;   
        }

        pushIn(stk, otherInterN);

        if (otherInterN == dstN)
        {
            popOutAll(stk);
            reachTime++;
            stk->sp --;   /*    back trace one node  */
            continue;
        }
        else
            InterNode(otherInterN, stk);

    }

        stk->sp --;

}


int    main()

{

    struct  stack   stk;

    initStk(&stk);
    pushIn(&stk, srcNode);  

    reachTime = 0;
    InterNode(srcNode, &stk);

    printf("\nNumber of all possible and unique routes = %d\n", reachTime);

}

2

Điều này có thể muộn, nhưng đây là phiên bản C # tương tự của thuật toán DFS trong Java từ Casey để duyệt cho tất cả các đường dẫn giữa hai nút bằng cách sử dụng ngăn xếp. Khả năng đọc tốt hơn với đệ quy như mọi khi.

    void DepthFirstIterative(T start, T endNode)
    {
        var visited = new LinkedList<T>();
        var stack = new Stack<T>();

        stack.Push(start);

        while (stack.Count != 0)
        {
            var current = stack.Pop();

            if (visited.Contains(current))
                continue;

            visited.AddLast(current);

            var neighbours = AdjacentNodes(current);

            foreach (var neighbour in neighbours)
            {
                if (visited.Contains(neighbour))
                    continue;

                if (neighbour.Equals(endNode))
                {
                    visited.AddLast(neighbour);
                    printPath(visited));
                    visited.RemoveLast();
                    break;
                }
            }

            bool isPushed = false;
            foreach (var neighbour in neighbours.Reverse())
            {
                if (neighbour.Equals(endNode) || visited.Contains(neighbour) || stack.Contains(neighbour))
                {
                    continue;
                }

                isPushed = true;
                stack.Push(neighbour);
            }

            if (!isPushed)
                visited.RemoveLast();
        }
    }
Đây là biểu đồ mẫu để kiểm tra:

    // Đồ thị mẫu. Số là id cạnh
    // 1 3       
    // A --- B --- C ----
    // | | 2 |
    // | 4 ----- D |
    // ------------------

1
tuyệt vời - về cách bạn đã thay thế đệ quy bằng phép lặp dựa trên ngăn xếp.
Siddhartha Ghosh

Tôi vẫn không hiểu nó, là neighbours.Reverse()gì? Là nó List<T>.Reverse ?

Tôi đã kiểm tra phiên bản không đệ quy này, nhưng nó có vẻ không chính xác. phiên bản đệ quy là tốt. có lẽ khi thay đổi để không đệ quy, một sai lầm nhỏ đã xảy ra
Arslan

@alim: Đồng ý, mã này chỉ đơn giản là bị hỏng. (Nó không loại bỏ chính xác các nút khỏi tập hợp đã truy cập khi backtracking và việc xử lý ngăn xếp dường như cũng bị rối loạn. Tôi đã cố gắng xem liệu nó có thể được sửa hay không, nhưng về cơ bản điều đó sẽ yêu cầu viết lại hoàn chỉnh.) đã thêm một câu trả lời với một giải pháp không đệ quy chính xác, hoạt động (bằng Python, nhưng nó sẽ tương đối dễ dàng để chuyển sang các ngôn ngữ khác).
Ilmari Karonen

@llmari Karonen, Rất vui, tôi sẽ kiểm tra, Làm tốt lắm.
arslan

1

Tôi đã giải quyết một vấn đề tương tự như vấn đề này gần đây, thay vì tất cả các giải pháp tôi chỉ quan tâm đến ngắn nhất.

Tôi đã sử dụng tìm kiếm lặp lại 'đầu tiên theo chiều rộng' sử dụng một hàng đợi trạng thái ', mỗi hàng trong số đó giữ một bản ghi chứa một điểm hiện tại trên biểu đồ và đường dẫn đến đó.

bạn bắt đầu với một bản ghi duy nhất trong hàng đợi, bản ghi này có nút bắt đầu và một đường dẫn trống.

Mỗi lần lặp qua mã sẽ đưa mục ra khỏi đầu danh sách và kiểm tra xem nó có phải là giải pháp hay không (nút đến là nút bạn muốn, nếu có, chúng tôi đã hoàn thành), nếu không, nó tạo ra một mục hàng đợi với các nút kết nối với nút hiện tại và các đường dẫn được sửa đổi dựa trên đường dẫn của nút trước đó, với bước nhảy mới được đính kèm ở cuối.

Bây giờ, bạn có thể sử dụng một cái gì đó tương tự, nhưng khi bạn tìm thấy giải pháp, thay vì dừng lại, hãy thêm giải pháp đó vào 'danh sách tìm thấy' của bạn và tiếp tục.

Bạn cần phải theo dõi danh sách các nút đã truy cập để không bao giờ quay lại chính mình nếu không bạn sẽ có một vòng lặp vô hạn.

nếu bạn muốn thêm một chút mã giả, hãy đăng một bình luận hoặc một cái gì đó, và tôi sẽ nói rõ hơn.


6
Tôi tin rằng nếu bạn chỉ quan tâm đến con đường ngắn nhất, thì Thuật toán Dijkstra là "giải pháp" :).
vicatcu

1

Tôi nghĩ bạn nên mô tả vấn đề thực sự của bạn đằng sau điều này. Tôi nói điều này bởi vì bạn yêu cầu một thứ gì đó hiệu quả về thời gian, nhưng câu trả lời cho vấn đề dường như tăng theo cấp số nhân!

Do đó, tôi sẽ không mong đợi một thuật toán tốt hơn một thứ gì đó theo cấp số nhân.

Tôi sẽ thực hiện backtracking và xem qua toàn bộ biểu đồ. Để tránh chu kỳ, hãy lưu tất cả các nút đã truy cập trên đường đi. Khi bạn quay lại, hãy bỏ đánh dấu nút.

Sử dụng đệ quy:

static bool[] visited;//all false
Stack<int> currentway; initialize empty

function findnodes(int nextnode)
{
if (nextnode==destnode)
{
  print currentway 
  return;
}
visited[nextnode]=true;
Push nextnode to the end of currentway.
for each node n accesible from nextnode:
  findnodes(n);
visited[nextnode]=false; 
pop from currenteay
}

Hay là sai?

chỉnh sửa: Ồ, và tôi quên mất: Bạn nên loại bỏ các lệnh gọi đệ quy bằng cách sử dụng ngăn xếp nút đó


Vấn đề thực sự của tôi là chính xác như tôi đã mô tả, chỉ với những bộ lớn hơn nhiều. Tôi đồng ý rằng điều này dường như phát triển theo cấp số nhân với kích thước của tập hợp.
Robert Groves

1

Nguyên tắc cơ bản là bạn không cần lo lắng về đồ thị, đây là vấn đề tiêu chuẩn được gọi là vấn đề kết nối động. Có các loại phương pháp sau mà bạn có thể đạt được các nút được kết nối hay không:

  1. Tìm kiếm nhanh
  2. Liên minh nhanh
  3. Thuật toán cải tiến (Kết hợp cả hai)

Đây là Mã C mà tôi đã thử với độ phức tạp thời gian tối thiểu O (log * n) Điều đó có nghĩa là đối với danh sách 65536 cạnh, nó yêu cầu 4 tìm kiếm và đối với 2 ^ 65536, nó yêu cầu 5 tìm kiếm. Tôi đang chia sẻ cách triển khai của mình từ thuật toán: Khóa học thuật toán từ trường đại học Princeton

MẸO: Bạn có thể tìm thấy giải pháp Java từ liên kết được chia sẻ ở trên với các giải thích phù hợp.

/* Checking Connection Between Two Edges */

#include<stdio.h>
#include<stdlib.h>
#define MAX 100

/*
  Data structure used

vertex[] - used to Store The vertices
size - No. of vertices
sz[] - size of child's
*/

/*Function Declaration */
void initalize(int *vertex, int *sz, int size);
int root(int *vertex, int i);
void add(int *vertex, int *sz, int p, int q);
int connected(int *vertex, int p, int q);

int main() //Main Function
{ 
char filename[50], ch, ch1[MAX];
int temp = 0, *vertex, first = 0, node1, node2, size = 0, *sz;
FILE *fp;


printf("Enter the filename - "); //Accept File Name
scanf("%s", filename);
fp = fopen(filename, "r");
if (fp == NULL)
{
    printf("File does not exist");
    exit(1);
}
while (1)
{
    if (first == 0) //getting no. of vertices
    {
        ch = getc(fp);
        if (temp == 0)
        {
            fseek(fp, -1, 1);
            fscanf(fp, "%s", &ch1);
            fseek(fp, 1, 1);
            temp = 1;
        }
        if (isdigit(ch))
        {
            size = atoi(ch1);
            vertex = (int*) malloc(size * sizeof(int));     //dynamically allocate size  
            sz = (int*) malloc(size * sizeof(int));
            initalize(vertex, sz, size);        //initialization of vertex[] and sz[]
        }
        if (ch == '\n')
        {
            first = 1;
            temp = 0;
        }
    }
    else
    {
        ch = fgetc(fp);
        if (isdigit(ch))
            temp = temp * 10 + (ch - 48);   //calculating value from ch
        else
        {
            /* Validating the file  */

            if (ch != ',' && ch != '\n' && ch != EOF)
            {
                printf("\n\nUnkwown Character Detected.. Exiting..!");

                exit(1);
            }
            if (ch == ',')
                node1 = temp;
            else
            {
                node2 = temp;
                printf("\n\n%d\t%d", node1, node2);
                if (node1 > node2)
                {
                    temp = node1;
                    node1 = node2;
                    node2 = temp;
                }

                /* Adding the input nodes */

                if (!connected(vertex, node1, node2))
                    add(vertex, sz, node1, node2);
            }
            temp = 0;
        }

        if (ch == EOF)
        {
            fclose(fp);
            break;
        }
    }
}

do
{
    printf("\n\n==== check if connected ===");
    printf("\nEnter First Vertex:");
    scanf("%d", &node1);
    printf("\nEnter Second Vertex:");
    scanf("%d", &node2);

    /* Validating The Input */

    if( node1 > size || node2 > size )
    {
        printf("\n\n Invalid Node Value..");
        break;
    }

    /* Checking the connectivity of nodes */

    if (connected(vertex, node1, node2))
        printf("Vertex %d and %d are Connected..!", node1, node2);
    else
        printf("Vertex %d and %d are Not Connected..!", node1, node2);


    printf("\n 0/1:  ");

    scanf("%d", &temp);

} while (temp != 0);

free((void*) vertex);
free((void*) sz);


return 0;
}

void initalize(int *vertex, int *sz, int size) //Initialization of graph
{
int i;
for (i = 0; i < size; i++)
{
    vertex[i] = i;
    sz[i] = 0;
}
}
int root(int *vertex, int i)    //obtaining the root
{
while (i != vertex[i])
{
    vertex[i] = vertex[vertex[i]];
    i = vertex[i];
}
return i;
}

/* Time Complexity for Add --> logn */
void add(int *vertex, int *sz, int p, int q) //Adding of node
{
int i, j;
i = root(vertex, p);
j = root(vertex, q);

/* Adding small subtree in large subtree  */

if (sz[i] < sz[j])
{
    vertex[i] = j;
    sz[j] += sz[i];
}
else
{
    vertex[j] = i;
    sz[i] += sz[j];
}

}

/* Time Complexity for Search -->lg* n */

int connected(int *vertex, int p, int q) //Checking of  connectivity of nodes
{
/* Checking if root is same  */

if (root(vertex, p) == root(vertex, q))
    return 1;

return 0;
}

Điều này dường như không giải quyết được vấn đề như đã hỏi. OP muốn tìm tất cả các đường dẫn đơn giản giữa hai nút, không chỉ để kiểm tra xem có tồn tại một đường dẫn hay không.
Ilmari Karonen

1

find_paths [s, t, d, k]

Câu hỏi này đã cũ và đã được trả lời. Tuy nhiên, không có thuật toán nào cho thấy có lẽ là một thuật toán linh hoạt hơn để đạt được điều tương tự. Vì vậy, tôi sẽ ném chiếc mũ của mình vào sàn đấu.

Cá nhân tôi thấy một thuật toán có dạng find_paths[s, t, d, k]hữu ích, trong đó:

  • s là nút bắt đầu
  • t là nút đích
  • d là độ sâu tối đa để tìm kiếm
  • k là số con đường cần tìm

Sử dụng dạng vô cực của ngôn ngữ lập trình của bạn cho dksẽ cung cấp cho bạn tất cả các đường dẫn§.

§ rõ ràng nếu bạn đang sử dụng đồ thị có hướng và bạn muốn tất cả các đường dẫn vô hướng giữa stbạn sẽ phải chạy theo cả hai cách:

find_paths[s, t, d, k] <join> find_paths[t, s, d, k]

Chức năng trợ giúp

Cá nhân tôi thích đệ quy, mặc dù nó có thể khó khăn đôi khi, dù sao trước tiên hãy xác định hàm trợ giúp của chúng tôi:

def find_paths_recursion(graph, current, goal, current_depth, max_depth, num_paths, current_path, paths_found)
  current_path.append(current)

  if current_depth > max_depth:
    return

  if current == goal:
    if len(paths_found) <= number_of_paths_to_find:
      paths_found.append(copy(current_path))

    current_path.pop()
    return

  else:
    for successor in graph[current]:
    self.find_paths_recursion(graph, successor, goal, current_depth + 1, max_depth, num_paths, current_path, paths_found)

  current_path.pop()

Chức năng chính

Với điều đó, chức năng cốt lõi là tầm thường:

def find_paths[s, t, d, k]:
  paths_found = [] # PASSING THIS BY REFERENCE  
  find_paths_recursion(s, t, 0, d, k, [], paths_found)

Đầu tiên, hãy lưu ý một số điều:

  • mã giả ở trên là sự kết hợp của các ngôn ngữ - nhưng hầu hết giống với python (vì tôi chỉ viết mã trong đó). Sao chép-dán chặt chẽ sẽ không hoạt động.
  • [] là danh sách chưa được khởi tạo, hãy thay thế danh sách này bằng danh sách tương đương cho ngôn ngữ lập trình bạn chọn
  • paths_foundđược thông qua bằng cách tham chiếu . Rõ ràng là hàm đệ quy không trả về bất cứ thứ gì. Xử lý điều này một cách thích hợp.
  • ở đây graphlà giả định một số dạng hashedcấu trúc. Có rất nhiều cách để triển khai một biểu đồ. Dù bằng cách nào, graph[vertex]bạn sẽ nhận được danh sách các đỉnh liền kề trong biểu đồ có hướng - điều chỉnh cho phù hợp.
  • điều này giả sử bạn đã xử lý trước để loại bỏ "khóa" (vòng lặp tự động), chu kỳ và nhiều cạnh

0

Đây là một suy nghĩ trong đầu tôi:

  1. Tìm một kết nối. (Tìm kiếm theo chiều sâu có lẽ là một thuật toán tốt cho việc này, vì độ dài đường dẫn không quan trọng.)
  2. Tắt phân đoạn cuối cùng.
  3. Cố gắng tìm một kết nối khác từ nút cuối cùng trước khi kết nối bị tắt trước đó.
  4. Goto 2 cho đến khi không còn kết nối nào nữa.

Điều này nói chung sẽ không hiệu quả: hoàn toàn có thể xảy ra hai hoặc nhiều đường đi giữa các đỉnh có cùng cạnh cuối. Phương pháp của bạn sẽ chỉ tìm thấy một trong những đường dẫn như vậy.
Ilmari Karonen

0

Theo như tôi có thể nói rằng các giải pháp được đưa ra bởi Ryan Fox ( 58343 , Christian ( 58444 ) và chính bạn ( 58461 ) là tốt như nó có được. Tôi không tin rằng truyền tải đầu tiên theo chiều rộng sẽ giúp ích trong trường hợp này, như bạn sẽ làm không có được tất cả đường dẫn. Ví dụ, với các cạnh (A,B), (A,C), (B,C), (B,D)(C,D)bạn sẽ nhận được đường dẫn ABDACD, nhưng không phải ABCD.


mweerden, Truyền tải đầu tiên theo chiều rộng mà tôi đã gửi sẽ tìm thấy TẤT CẢ các đường dẫn trong khi tránh bất kỳ chu kỳ nào. Đối với biểu đồ mà bạn đã chỉ định, việc triển khai tìm thấy chính xác tất cả ba đường dẫn.
Casey Watson

Tôi đã không hoàn toàn đọc mã của bạn và cho rằng bạn đã sử dụng truyền tải đầu tiên theo chiều rộng (vì bạn đã nói như vậy). Tuy nhiên, khi kiểm tra kỹ hơn sau nhận xét của bạn, tôi nhận thấy rằng thực tế không phải vậy. Nó thực sự là một cuộc vượt qua chiều sâu không có ký ức giống như của Ryan, Christian và Robert.
mweerden 12/09/08

0

Tôi đã tìm ra một cách để liệt kê tất cả các đường dẫn bao gồm vô số đường dẫn có chứa các vòng lặp.

http://blog.vjeux.com/2009/project/project-shortest-path.html

Tìm đường dẫn & chu kỳ nguyên tử

Definition

Những gì chúng tôi muốn làm là tìm tất cả các con đường có thể đi từ điểm A đến điểm B. Vì có các chu trình liên quan, bạn không thể chỉ đi qua và liệt kê tất cả. Thay vào đó, bạn sẽ phải tìm đường nguyên tử không lặp lại và các chu kỳ nhỏ nhất có thể (bạn không muốn chu kỳ của mình lặp lại chính nó).

Định nghĩa đầu tiên tôi đưa ra về đường nguyên tử là đường không đi qua cùng một nút hai lần. Tuy nhiên, tôi phát hiện ra rằng đó là không tận dụng tất cả các khả năng. Sau một số phản xạ, tôi nhận ra rằng các nút không quan trọng, tuy nhiên các cạnh thì có! Vì vậy, một con đường nguyên tử là một con đường không đi qua cùng một cạnh hai lần.

Định nghĩa này rất tiện dụng, nó cũng áp dụng cho các chu kỳ: chu kỳ nguyên tử của điểm A là một đường nguyên tử đi từ điểm A và kết thúc đến điểm A.

Thực hiện

Atomic Paths A -> B

Để có được tất cả các đường đi bắt đầu từ điểm A, chúng ta sẽ duyệt đệ quy đồ thị từ điểm A. Trong khi đi qua một con, chúng ta sẽ tạo một liên kết con -> cha để biết tất cả các cạnh của chúng ta. đã vượt qua. Trước khi đến phần con đó, chúng ta phải duyệt qua danh sách liên kết đó và đảm bảo rằng cạnh được chỉ định chưa được đi qua.

Khi chúng tôi đến điểm đích, chúng tôi có thể lưu trữ con đường mà chúng tôi đã tìm thấy.

Freeing the list

Sự cố xảy ra khi bạn muốn giải phóng danh sách liên kết. Về cơ bản nó là một cái cây được xâu chuỗi theo thứ tự ngược lại. Một giải pháp sẽ là liên kết kép danh sách đó và khi tất cả các đường dẫn nguyên tử được tìm thấy, hãy giải phóng cây khỏi điểm xuất phát.

Nhưng một giải pháp thông minh là sử dụng phép đếm tham chiếu (lấy cảm hứng từ Bộ sưu tập rác). Mỗi khi bạn thêm một liên kết đến trang gốc, bạn sẽ thêm một liên kết vào số lượng tham chiếu của nó. Sau đó, khi bạn đến cuối con đường, bạn đi lùi và tự do trong khi số tham chiếu bằng 1. Nếu nó cao hơn, bạn chỉ cần loại bỏ một và dừng lại.

Atomic Cycle A

Tìm kiếm chu kỳ nguyên tử của A cũng giống như tìm đường nguyên tử từ A đến A. Tuy nhiên, chúng ta có thể thực hiện một số cách tối ưu hóa. Đầu tiên, khi chúng ta đến điểm đích, chúng ta chỉ muốn lưu đường đi nếu tổng chi phí của các cạnh là âm: chúng ta chỉ muốn đi qua các chu trình hấp thụ.

Như bạn đã thấy trước đây, toàn bộ đồ thị đang được duyệt qua khi tìm kiếm đường nguyên tử. Thay vào đó, chúng ta có thể giới hạn vùng tìm kiếm đối với thành phần được kết nối mạnh mẽ có chứa A. Việc tìm kiếm các thành phần này yêu cầu một đường di chuyển đơn giản của đồ thị với thuật toán của Tarjan.

Kết hợp các con đường nguyên tử và chu kỳ

Tại thời điểm này, chúng ta có tất cả các con đường nguyên tử đi từ A đến B và tất cả các chu kỳ nguyên tử của mỗi nút, hãy để chúng ta sắp xếp mọi thứ để có được con đường ngắn nhất. Từ bây giờ chúng ta sẽ nghiên cứu cách tìm ra sự kết hợp tốt nhất của các chu kỳ nguyên tử trong một con đường nguyên tử.


Điều này dường như không trả lời câu hỏi như đã hỏi.
Ilmari Karonen

0

Như mô tả của một số áp phích khác, tóm lại, vấn đề là sử dụng thuật toán tìm kiếm theo chiều sâu để tìm kiếm đệ quy trên biểu đồ cho tất cả các tổ hợp đường dẫn giữa các nút cuối giao tiếp.

Bản thân thuật toán bắt đầu với nút bắt đầu mà bạn cung cấp cho nó, kiểm tra tất cả các liên kết đi của nó và tiến trình bằng cách mở rộng nút con đầu tiên của cây tìm kiếm xuất hiện, tìm kiếm ngày càng sâu hơn cho đến khi tìm thấy nút đích hoặc cho đến khi nó gặp một nút mà không có con.

Sau đó, tìm kiếm sẽ quay trở lại nút gần đây nhất mà nó vẫn chưa khám phá xong.

Tôi đã viết blog về chủ đề này khá gần đây, đăng một ví dụ về triển khai C ++ trong quá trình này.


0

Thêm vào câu trả lời của Casey Watson, đây là một triển khai Java khác,. Khởi tạo nút đã truy cập bằng nút bắt đầu.

private void getPaths(Graph graph, LinkedList<String> visitedNodes) {
                LinkedList<String> adjacent = graph.getAdjacent(visitedNodes.getLast());
                for(String node : adjacent){
                    if(visitedNodes.contains(node)){
                        continue;
                    }
                    if(node.equals(END)){
                        visitedNodes.add(node);
                        printPath(visitedNodes);
                        visitedNodes.removeLast();
                    }
                    visitedNodes.add(node);
                    getPaths(graph, visitedNodes);
                    visitedNodes.removeLast();  
                }
            }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.