1

I have a list of tuples containing sources and destinations I need to make sure there are no "round routes"

For example:

[(1,2), (3,4), (2,3)]. is OK

but

[(1,2), (3,4), (2,3), (3,1)]. 

It is not OK as we can go from 1 → 2 → 3 → 1

I got nostalgic and remembered my computer science degree, so I thought of Graph data structure. Unfortunately I could not find a good implementation for it using python, and all examples I found googling (and here on stackoverflow) are only for finding shortest path.

Any ideas how can I implement this?

martineau
  • 119,623
  • 25
  • 170
  • 301
Ido Barash
  • 4,856
  • 11
  • 41
  • 78
  • What have you tried? You need to build a graph (which is just a dictionary), then you need to do a depth-first tree traversal to see if you go back to any previously visited nodes. – Tim Roberts Apr 28 '21 at 06:18
  • 2
    In computer science terms, you're trying to show that your "directed graph" is a "directed acyclic graph". – Tim Roberts Apr 28 '21 at 06:19

4 Answers4

1

Here is an implementation in Python that works pretty well (also available in Java and C++, but I only tried the first case). https://www.techiedelight.com/check-given-digraph-dag-directed-acyclic-graph-not/

Javier A
  • 539
  • 2
  • 12
0

If I understand correctly then the problem is when you can get to a starting point. I would start with a plain python for the sake of simplicity and explicity. Obvious brute force solution is to check all possible destinations for every source presented in data. Of course you need to organize your data a little. Then it's just a chaining. The code below implements this approach.

def get_all_sources(data):

    ans = dict()
    for src, dst in data:
        ans.setdefault(src, set()).add(dst)

    return ans


def get_all_possible_destinations(src, all_src, ans=None):

    ans = set() if ans is None else ans
    for dst in all_src.get(src, set()):
        if dst in ans:
            continue
        ans.add(dst)
        get_all_possible_destinations(dst, all_src, ans)

    return ans


def pipeline_source_by_source(data):

    all_src = get_all_sources(data)

    for src in all_src:
        all_possible_destiations = get_all_possible_destinations(src, all_src)
        if src in all_possible_destiations:
            print(f"found problem: {src} -> {src}")
            break
    else:
        print('no problems found')


if __name__ == '__main__':

    data_list = [
        [(1, 2)],
        [(1, 2), (2, 3)],
        [(1, 2), (3, 4), (2, 3)],
        [(1, 2), (3, 4), (2, 3), (3, 1)],
        [(5, 6), (5, 7), (5, 8), (5, 9), (9, 10), (10, 5)],
        [(5, 6), (5, 7), (5, 8), (5, 9), (9, 10), (10, 15)]
    ]

    for idx, data in enumerate(data_list):
        print(idx)
        pipeline_source_by_source(data)

Result:

0
no problems found
1
no problems found
2
no problems found
3
found problem: 1 -> 1
4
found problem: 5 -> 5
5
no problems found
0

This is a bit late, but gives a shorter solution (less Python lines). The rationale is to build a dictionary of destinations directly reachable from a source (indexed by the sources). Then from any source, browse the possible destinations, and store any already seen destination in a set. As soon as a new destination has been seen there is a loop.

Python code could be:

def build_dict(lst):
    d = dict()
    for src, dst in lst:
        if src not in d:
            d[src] = []
        d[src].append(dst)
    return d

def dict_loops(d, start=None, seen=None):
    if start is None:
        return any(dict_loops(d, elt, None) for elt in d.keys())
    if start not in d:
        return False
    if seen is None:
        seen = {start}
    for hop in d[start]:
        if hop in seen:
            return True
        if dict_loops(d, hop, seen):
            return True
    return False

def lst_loops(lst):
    return dict_loops(build_dict(lst))

It gives as expected:

>>> lst_loops([(1,2), (3,4), (2,3)])
False
>>> lst_loops([(1,2), (3,4), (2,3), (3,1)])
True
>>> 

meaning no loop in first list, and at least one in second.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
0

You can use a recursive generator function:

def no_cycles(graph):
  def has_cycles(n, s = []):
     if n in s:
        yield (False, n)
     else:
        yield (True, n)
        yield from [i for a, b in graph for i in has_cycles(b, s+[n]) if a == n]
  return all(a for a, _ in has_cycles(graph[0][0]))

graphs = [[(1, 2)], [(1, 2), (2, 3)], [(1, 2), (3, 4), (2, 3)], [(1, 2), (3, 4), (2, 3), (3, 1)], [(5, 6), (5, 7), (5, 8), (5, 9), (9, 10), (10, 5)], [(5, 6), (5, 7), (5, 8), (5, 9), (9, 10), (10, 15)]]
result = [no_cycles(i) for i in graphs]

Output:

[True, True, True, False, False, True]
Ajax1234
  • 69,937
  • 8
  • 61
  • 102