How to order tuples by matching the first and last values of each "(a, b), (b, c), (c, d)"

Viewed 831

I have a list of tuples containing a pair of integers. I want to order them so that each tuple is ordered relative to the tuple before and after it having the same corresponding value. The first number in the tuple is the second number in the tuple before it and the second number in the tuple is the first number in the tuple after it.

(a, b), (b, c), (c, d)

For example the following list

[(8,7),(2,8),(3,5),(11,2),(5,11)]

should be ordered

[(3,5),(5,11),(11,2),(2,8),(8,7)]

All input lists of tuples have exactly one possible ordering. There are no duplicate tuples and no value will ever show up more than once.

I have tried a few options but the most promising one so far has a big flaw which is illustrated below using a larger list of tuples.

pairs = [(1024, 2048), (32768, 65536), (36, 12), (16, 32), (256, 512), (9, 18), (32, 64), (16384, 32768), (8, 16), (64, 128), (512, 1024), (128, 256), (8192, 16384), (2048, 4096), (18, 36), (4096, 8192), (4, 8), (27, 9), (12, 4)]

sorted_result = pairs.copy()

for pair in correct_pairs:

    pair_to_insert = sorted_result.pop(sorted_result.index(pair))
    
    for index, comparison_pair in enumerate(sorted_result):
        
        # if last number of the tuple to insert matches the first number of the compared tuple insert the tuple before
        if pair_to_insert[1] == comparison_pair[0]:
            sorted_result.insert(index, pair_to_insert)
            break
            
        # if the first number of the tuple to insert matches the last number of the compared tuple insert the tuple after
        if  pair_to_insert[0] == comparison_pair[1]:
            sorted_result.insert(index+1, pair_to_insert)
            break

print(sorted_result)

Output

[(12, 4), (4, 8), (8, 16), (16, 32), (32, 64), (64, 128), (128, 256), (4096, 8192), (8192, 16384), (16384, 32768), (32768, 65536), (256, 512), (512, 1024), (1024, 2048), (2048, 4096), (27, 9), (9, 18), (18, 36), (36, 12)]

The sorted result contains chains of ordered tuples that are themselves out of order.

I think the approach I'm taking is flawed because any element that isn't first or last in the ordering can always be inserted before or after two respective elements and it depends on which of these shows up first as to where it gets inserted.

Any ideas to solve this? Is there a simpler way to do this using inbuilt functions?

6 Answers

Using an adjacency matrix seems to be a solution to your problem:

# Create an adjacency matrix to find the next value fast 
adjacency_matrix = {pair[0]: pair for pair in pairs}
# The first element can be found being the first element of the pair not 
# present in the second elements
first_key = set(pair[0] for pair in pairs).difference(pair[1] for pair in pairs)

# Simply pop the elements from the adjacency matrix
sorted_pairs = [adjacency_matrix.pop(first_key.pop())]
while adjacency_matrix:
    # sorted_pairs[-1][1] takes the second element of 
    # the last pair inserted
    sorted_pairs.append(adjacency_matrix.pop(sorted_pairs[-1][1]))

print(sorted_pairs)

If each tuple is considered as an edge in a graph, then the problem is equivalent to finding a topological ordering of the edges.

from collections import defaultdict 
  
class Graph: 
    def __init__(self) -> None: 
        self.adj_list = defaultdict(list) #dictionary containing adjacency list 
  
    def add_edge(self, u, v) -> None: 
        self.adj_list[u].append(v) 
  
    def topological_sort(self) -> list: 
        visited = set()
        reverse_topo = list() 
  
        vertices = set(self.adj_list.keys())
        for vertex in vertices: 
            if vertex not in visited:
                self._topological_sort_util(vertex, visited, reverse_topo) 
        return list(reversed(reverse_topo))
    
    def _topological_sort_util(self, vertex, visited: set, reverse_topo: list) -> None: 
        visited.add(vertex)
        for adj_vertex in self.adj_list[vertex]: 
            if adj_vertex not in visited: 
                self._topological_sort_util(adj_vertex, visited, reverse_topo) 
        reverse_topo.append(vertex)


pairs = [(1024, 2048), (32768, 65536), (36, 12), (16, 32), (256, 512), (9, 18), (32, 64), (16384, 32768), (8, 16), (64, 128), (512, 1024), (128, 256), (8192, 16384), (2048, 4096), (18, 36), (4096, 8192), (4, 8), (27, 9), (12, 4)]

g = Graph() 
for edge in pairs:
    g.add_edge(edge[0], edge[1])

vertices_topo_sorted = g.topological_sort() 
print("Graph vertices sorted in topological order:")
print(vertices_topo_sorted)

Output

Graph vertices sorted in topological order:
[27, 9, 18, 36, 12, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]

Reconstruct the edge tuples

To reconstruct the edge tuples from the topologically ordered vertices, you could do the following.

edge_tuples = [(u, v) for u, v in zip(vertices_topo_sorted[0:], vertices_topo_sorted[1:])]
print("\nEdge tuples in topological order:")
print(edge_tuples)

Output

Edge tuples in topological order:
[(27, 9), (9, 18), (18, 36), (36, 12), (12, 4), (4, 8), (8, 16), (16, 32), (32, 64), (64, 128), (128, 256), (256, 512), (512, 1024), (1024, 2048), (2048, 4096), (4096, 8192), (8192, 16384), (16384, 32768), (32768, 65536)]

I've put something together using a generator function, there may be a more elegant solution but this is mine:

pairs = [(1024, 2048), (32768, 65536), (36, 12), (16, 32), (256, 512), (9, 18), (32, 64), (16384, 32768), (8, 16), (64, 128), (512, 1024), (128, 256), (8192, 16384), (2048, 4096), (18, 36), (4096, 8192), (4, 8), (27, 9), (12, 4)]

a_values,b_values = zip(*pairs)

#find index of first 'a' which does not have a matching 'b'
index = next((a_values.index(value) for value in a_values if value not in b_values),None)

result = []
while index is not None:
    pair = pairs[index]
    result.append(pair)
    # find first pair where 'a' is equal to the current pair's 'b'
    index = next((a_values.index(value) for value in a_values if pair[1] == value),None)
    
print(result)  

assuming unique solution exist I found this one

def fun(pairs):
    data = dict(pairs)
    candidatos=data.keys()-data.values()
    print(candidatos)
    initial = candidatos.pop()
    while data:
        nextpoint = data[initial]
        yield initial, nextpoint
        del data[initial]
        initial = nextpoint

I make a dictionary out of the pair that in the case mean current:next, then I look for the value such that isn't the next of anyone in candidatos, take it and build the path from there

>>> x=[(8,7),(2,8),(3,5),(11,2),(5,11)]
>>> list(fun(x))
{3}
[(3, 5), (5, 11), (11, 2), (2, 8), (8, 7)]
>>> pairs = [(1024, 2048), (32768, 65536), (36, 12), (16, 32), (256, 512), (9, 18), (32, 64), (16384, 32768), (8, 16), (64, 128), (512, 1024), (128, 256), (8192, 16384), (2048, 4096), (18, 36), (4096, 8192), (4, 8), (27, 9), (12, 4)]
>>> list(fun(pairs))
{27}
[(27, 9), (9, 18), (18, 36), (36, 12), (12, 4), (4, 8), (8, 16), (16, 32), (32, 64), (64, 128), (128, 256), (256, 512), (512, 1024), (1024, 2048), (2048, 4096), (4096, 8192), (8192, 16384), (16384, 32768), (32768, 65536)]
>>> 

Assuming the solution exists:

# output
res = [pairs[0]]
# sorted ids (to exclude in further processing)
ids = [0]
i = 1
while len(ids)<len(pairs):
    if(i not in ids):
        if(res[0][0] == pairs[i][1]):
            res = [pairs[i]] + res
            ids = [i] + ids
        elif(res[-1][1] == pairs[i][0]):
            res = res + [pairs[i]]
            ids = ids + [i]
    i += 1
    if(i > len(pairs)-1): i = 1

If 3rd party libraries are an option, then you can use networkx.

One way to think of this problem (as others mentioned) is to treat the list

[(8, 7), (2, 8), (3, 5), (11, 2), (5, 11)]

as a list of edges between nodes of a directed graph. You want to create the topological sort of the edges of this graph. This can be directly achieved with networkx's topological_sort algorithm:

import networkx as nx

edges = [(8, 7), (2, 8), (3, 5), (11, 2), (5, 11)]

G = nx.DiGraph(edges)

print(list(nx.topological_sort(nx.line_graph(G))))

will give:

[(3, 5), (5, 11), (11, 2), (2, 8), (8, 7)]
Related