Related
So I have followed Wikipedia's pseudocode for Dijkstra's algorithm as well as Brilliants. https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm#Pseudocode https://brilliant.org/wiki/dijkstras-short-path-finder/. Here is my code which doesn't work. Can anyone point in the flaw in my code?
# Uses python3
from queue import Queue
n, m = map(int, input().split())
adj = [[] for i in range(n)]
for i in range(m):
u, v, w = map(int, input().split())
adj[u-1].append([v, w])
adj[v-1].append([u, w])
x, y = map(int, input().split())
x, y = x-1, y-1
q = [i for i in range(n, 0, -1)]
#visited = set()
# visited.add(x+1)
dist = [float('inf') for i in range(len(adj))]
dist[x] = 0
# print(adj[visiting])
while len(q) != 0:
visiting = q.pop()-1
for i in adj[visiting]:
u, v = i
dist[u-1] = dist[visiting]+v if dist[visiting] + \
v < dist[u-1] else dist[u-1]
# print(dist)
if dist[y] != float('inf'):
print(dist[y])
else:
print(-1)
Your algorithm is not implementing Dijkstra's algorithm correctly. You are just iterating over all nodes in their input order and updating the distance to the neighbors based on the node's current distance. But that latter distance is not guaranteed to be the shortest distance, because you iterate some nodes before their "turn". Dijkstra's algorithm specifies a particular order of processing nodes, which is not necessarily the input order.
The main ingredient that is missing from your algorithm, is a priority queue. You did import from Queue, but never use it. Also, it lacks the marking of nodes as visited, a concept which you seemed to have implemented a bit, but which you commented out.
The outline of the algorithm on Wikipedia explains the use of this priority queue in the last step of each iteration:
Otherwise, select the unvisited node that is marked with the smallest tentative distance, set it as the new "current node", and go back to step 3.
There is currently no mechanism in your code that selects the visited node with smallest distance. Instead it picks the next node based on the order in the input.
To correct your code, please consult the pseudo code that is available on that same Wikipedia page, and I would advise to go for the variant with priority queue.
In Python you can use heapq for performing the actions on the priority queue (heappush, heappop).
I was implementing the Kruskel's algorithm in Python. But it isn't giving correct answers for dense graphs. Is there any flaw in the logic??
The algo I am using is this:
1) Store all vertices in visited
2) Sort all the edges wrt their weights
3) Keep picking the smallest edge until all vertices, v, have visited[v]=1
This is what I have tried:
def Kruskal(weighted_graph):
visited = {}
for u,v,_ in weighted_graph:
visited[u] = 0
visited[v] = 0
sorted_edges = deque(sorted(weighted_graph, key = lambda x:x[2]))
mstlist = []
sumi=0
while 0 in visited.values():
u,v,w = sorted_edges.popleft()
if visited[u] == 0 or visited[v] == 0:
mstlist.append((u,v))
visited[u] = 1
visited[v] = 1
sumi += w
return (sumi,mstlist)
input is a list of tuples..a single tuple looks like this (source,neighbor,weight)
The Minimum spanning tree sum which I am calculating is coming out to be wrong for dense graphs. Please help. Thank you!
Your condition for adding the edge is if visited[u] == 0 or visited[v] == 0, so you require that one of the adjacent nodes is not connected to any edge you have added to your MST so far. For the algorithm to work correctly, however, it is sometimes necessary to add edges even if you have already "visited" both nodes. Consider this very simple graph:
[
(A, B, 2),
(B, C, 3),
(C, D, 1),
]
visual representation:
[A]---(2)---[B]---(3)---[C]---(1)---[D]
Your algorithm would first add the edge (C, D), marking C and D as visited.
Then, it would add the edge (A, B), marking A and B as visited.
Now, you're only left with the edge (B, C). The MST for this graph obviously contains this edge. But your condition fails -- both B and C are marked as visited. So, your algorithm doesn't add that edge.
In conclusion, you need to replace that check. You should check whether the two nodes that the current edge connects are already connected by the edges that you have added to your MST so far. If they are, you skip it, otherwise you add it.
Usually, disjoint-set data structures are used for implementing this with a good run time complexity (see the pseudocode on wikipedia).
However, your code so far already has bad run time complexity as 0 in visited.values() has to linearly search through the values of the dictionary until it either reaches the end or finds an element with value 0, so it might be enough for you to do something simpler.
You can find some implementations of the algorithm using disjoint-set data structures on the internet, e.g. here.
This question deals with the A* algorithm and three variations thereof:
max-depth (only search within a fix depth away from the starting vertex)
multi-target (search for multiple targets rather than a single goal)
multi-path (search for the first n paths - may share some vertices - from the start to the target)
To address my question I shall use python (v3+) as it lends itself to readability. There are a host of graph data structures (AM, AL, DOK, CRS, etc, etc). As A* only performs operations on the vertex set (under the assumption that the vertex set has local knowledge of predecessors and successors), I will use an adjacency list... or more specifically an adjacency dictionary (hash) e.g.:
vertex_set = {
"a": {"b", "c"},
"b": {"c"},
...
}
To provide a launching point, an implementation of plain A* is provided below:
A*
helpers
from math import inf
def a_star_distance_between(start, goal):
# cost_estimate may go here
# but I set it to a constant zero transforming A* to a greedy breadth first
# search as this estimate will vary by your use case
cost_estimate = 0
return cost_estimate
def reconstruct_path(came_from, current):
total_path = [current]
while current in came_from.keys():
current = came_from[current]
total_path.append(current)
total_path.reverse()
return total_path
def a_star_heuristic_between(a, b):
# your heuristic goes here
# dummy heuristic just for functionality
heuristic = len(vertex_set[a]) + len(vertex_set[b])
return 1 / heuristic
def a_star_lowest_f_score(f_scores, nodes_currently_discovered):
f_min_val = inf
f_min_key = ""
for node in nodes_currently_discovered:
val = f_scores[node]
if val < f_min_val:
f_min_key = node
f_min_val = val
return f_min_key
A* normal
def a_star(start, stop):
nodes_already_evaluated = set() # a.k.a. closed set
nodes_currently_discovered = {start} # a.k.a. open set
came_from = dict()
# for each node, cost of getting from start node to that node
g_score = {v: inf for v in list(vertex_set.keys())}
g_score[start] = 0
# for each node, cost of getting from the start node to the goal by passing through that node
f_score = {v: inf for v in list(vertex_set.keys())}
f_score[start] = 1 # normally a_star_heuristic_between(start, stop), 1 here because of hard coded value above
while nodes_currently_discovered:
current = a_star_lowest_f_score(f_score, nodes_currently_discovered)
if current == stop:
return reconstruct_path(came_from, current)
nodes_currently_discovered.remove(current)
nodes_already_evaluated.add(current)
for neighbor in vertex_set[current]:
if neighbor in nodes_already_evaluated:
continue
if neighbor not in nodes_currently_discovered:
nodes_currently_discovered.add(neighbor)
tentative_g_score = g_score[current] + a_star_distance_between(current, neighbor)
if tentative_g_score >= g_score[neighbor]:
continue # not a better path
# best path until now
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = g_score[neighbor] + a_star_heuristic_between(neighbor, stop)
of the three variants addressed above, the easiest to make from the above implementation is (2) multi-target:
A* multi-target
from statistics import mean
def multi_target_a_star(start, stops):
# CHANGE
stops_paths = {stop: None for stop in stops}
nodes_already_evaluated = set() # a.k.a. closed set
nodes_currently_discovered = {start} # a.k.a. open set
came_from = dict()
# for each node, cost of getting from start node to that node
g_score = {v: inf for v in list(vertex_set.keys())}
g_score[start] = 0
# for each node, cost of getting from the start node to the goal by passing through that node
f_score = {v: inf for v in list(vertex_set.keys())}
f_score[start] = 1 # normally a_star_heuristic_between(start, stop), 1 here because of hard coded value
while nodes_currently_discovered:
current = a_star_lowest_f_score(f_score, nodes_currently_discovered)
if current == stop:
# CHANGE
stop_paths[current] = reconstruct_path(came_from, current)
if all([v != None for k, v in stops_paths.items()]):
return stops_paths
nodes_currently_discovered.remove(current)
nodes_already_evaluated.add(current)
for neighbor in vertex_set[current]:
if neighbor in nodes_already_evaluated:
continue
if neighbor not in nodes_currently_discovered:
nodes_currently_discovered.add(neighbor)
tentative_g_score = g_score[current] + a_star_distance_between(current, neighbor)
if tentative_g_score >= g_score[neighbor]:
continue # not a better path
# best path until now
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
# CHANGE
f_score[neighbor] = g_score[neighbor] + mean([a_star_heuristic_between(neighbor, stop) for stop in stops])
A* depth lock
def a_star(start, stop, max_depth=inf):
nodes_already_evaluated = set() # a.k.a. closed set
nodes_currently_discovered = {start} # a.k.a. open set
came_from = dict()
# for each node, cost of getting from start node to that node
g_score = {v: inf for v in list(vertex_set.keys())}
g_score[start] = 0
# for each node, cost of getting from the start node to the goal by passing through that node
f_score = {v: inf for v in list(vertex_set.keys())}
f_score[start] = 1 # normally a_star_heuristic_between(start, stop), 1 here because of hard coded value above
# keep track of distance. This is not the most efficient way to do so. However it lets us not have to modify our distance and heuristic functions.
d_score = {v: inf for v in list(vertex_set.keys())}
d_score[start] = 0
while nodes_currently_discovered:
current = a_star_lowest_f_score(f_score, nodes_currently_discovered)
if current == stop:
return reconstruct_path(came_from, current)
nodes_currently_discovered.remove(current)
nodes_already_evaluated.add(current)
# CHANGE: test for depth
if d_score[current] + 1 > max_depth:
# NOTE: at this point current will NOT be re-evaluated again even if there is a path where getting to current is less than max_depth
# this stems from current being placed in nodes_already_evaluated as well as that the node and distance are not kept together in a tuple, e.g. (node, dist) - which requires updating a couple of functions.
continue
for neighbor in vertex_set[current]:
if neighbor in nodes_already_evaluated:
continue
if neighbor not in nodes_currently_discovered:
nodes_currently_discovered.add(neighbor)
# CHANGE
d_score[neighbor] = d_score[current] + 1
tentative_g_score = g_score[current] + a_star_distance_between(current, neighbor)
if tentative_g_score >= g_score[neighbor]:
continue # not a better path
# best path until now
came_from[neighbor] = current
g_score[neighbor] = tentative_g_score
f_score[neighbor] = g_score[neighbor] + a_star_heuristic_between(neighbor, stop)
Question
My question is how to implement a version** (see note) of A* that can be called as follows:
a_star(start, stops, max_depth, num_paths, max_duration)
** NOTE: depending on the changes made, the resultant path search may no longer be comparable to A* as it may lose completeness. I use "version" loosely to simply denote that in this case it was based off of A*.
where:
start: starting vertex
stops: a list of vertices to which to find a path from start (e.g. start -> stops[0], start -> stops[1], ..., start -> stops[-1] )
max_depth: an integer representing the maximum path length that should be explored, e.g. max_depth=3 start -(1)-> a -(2)-> b -(3)-> stop
num_paths: the maximum number of paths to find between each start and stop, e.g. if len(stops)=2 then there should be at most 6 paths
max_duration: the maximum run time allowed before the function terminates and returns what has been found so far
I know that each of these variants (excluding max-depth) are main topics in relation to path finding. Multi-path has received more attention with various underlying premises. Some approaches attempt to send multiple agents such as Bee-Sensor. A more simplistic approach simply removes those vertices in the first found path from the graph and then re-run the algorithm on the sub graph (which I find very unsatisfactory). If a --> b --> c --> e and a --> b --> d --> e are paths, I would want both to found, rather than discarding the vertices b and c from evaluation.
One of the most interesting recent contributions comes from Yin and Yang, 2015. However, similar to the aforementioned naive approach of removing vertices of found paths, this method finds paths with greater variance (great for their use case, not for mine).
I have implemented my stated goal for DFS and BFS. However the framework for a-star makes it very difficult for me to see how to get then the second, third, etc best path.
Current thoughts
For multi-path I initially thought storing a simple list of paths would be sufficient and checking to see if the length of this met the desired amount. However, this does not work as the way the visited nodes are kept tracked of are in two sets: the 'closed' (nodes_already_evaluated) and 'open' (nodes_currently_discovered) sets. If the path A* returns is a --> b --> c, then at this point the closed set has at least {a, b, c} and we do not wish to remove b from evaluation as a -- > b -- d --> c might be the second best path. However including both b and the goal (c) in the open set just results in making the same path twice and recomputing already determined values. Especially if this multi-path approach is mixed with multi-target.
A valid question might be "why would you want to have a multi-path, multi-target" algorithm where you could just parallel launch multi-path single target searches. In the case when the distance function is constant or unknown, A* becomes a greedy breadth first search algorithm, hence the explored frontier is the same regardless of the final target. The above given multi-target implementation just keeps expanding the frontier until all targets are reached, which is more efficient.
I would greatly appreciate any guidance offered to finding a path finding function with the previously listed arguments (start, stops, max_depth, num_paths, duration) that at least contains a heuristic function and has competitive runtime.
For testing purposes I provide the following small graph:
vertex_set = {
1: {3, 4, 13, 21},
2: {3, 20},
3: {1, 2, 4, 5, 23},
4: {1, 3, 6},
5: {3, 7},
6: {4, 8, 16, 23},
7: {5, 9},
8: {6, 10, 11},
9: {7, 12},
10: {13, 14, 15},
11: {15, 16},
12: {9, 20},
13: {1, 10, 17},
14: {10, 17, 18},
15: {10, 11},
16: {6, 11, 18, 19, 20},
17: {13, 14},
18: {14, 16, 21},
19: {16, 22},
20: {2, 12, 16},
21: {1, 18},
22: {19},
23: {3, 6, 22}
}
Notes:
#tobias_k has pointed out several optimizations to the above code. Such as using a priority heap for finding the lowest f score vertex.
In addition I wish to point out that the above code is not for any serious production purposes. Rather, as stated initially, an implementation in a more readable language to help the discussion of how to modify the core concepts of the A* algorithm.
There are other optimizations. For example, for the depth lock variant (searching for paths of <= a fixed length), it would save both memory and time to not store all distances in d_score; rather the nodes_already_evaluated and nodes_currently_discovered (closed and open sets respectively) could store tuples of (vertex, distance_when_discovered). This would allow a vertex that is reachable by two different vertices at different depths to be added to the nodes_currently_discovered (open set) more than once - which is putatively desirable feature depending on use case.
Along these lines, #tobias_k suggests also storing the f_score of a vertex in a tuple for the priority heap.
I do appreciate insights into optimizations. However, that is not the primary purpose of the question.
In the networkx python package, is there a way to find all node cuts of minimal size consisting of only nodes from one set in a bipartite graph? For example, if the two sides of a bipartite graph are A and B, how might I go about finding all minimal node cuts consisting of nodes entirely from set B? The following code I have works but it's extremely slow:
def get_one_sided_cuts(G, A, B):
#get all cuts that consist of nodes exclusively from B which disconnect
#nodes from A
one_sided_cuts = []
seen = []
l = list(combinations(A, 2))
for x in l:
s = x[0]
t = x[1]
cut = connectivity.minimum_st_node_cut(G, s, t)
if set(cut).issubset(B) and (cut not in seen):
one_sided_cuts.append(cut)
seen.append(cut)
#find minimum cut size
cur_min = float("inf")
for i in one_sided_cuts:
if len(i) < cur_min:
cur_min = len(i)
one_sided_cuts = [x for x in one_sided_cuts if len(x) == cur_min]
return one_sided_cuts
Note that this actually only checks if there is a minimal cut which, if removed, would disconnect two nodes in A only. If your solution does this (instead of finding a cut that will separate any two nodes) that's fine too. Any ideas on how to do this more efficiently?
As stated in the comment, there are a couple of interpretations of “all node cuts of minimal size consisting of only nodes from one set in a bipartite graph”. It either means
All node cuts of minimum size when restricting cuts to be in one set of the bipartite graph, or
All node cuts in an unconstrained sense (consisting of nodes from A or B) that happen to completely lie in B.
From your code example you are interested in 2. According to the docs, there is a way to speed up this calculation, and from profile results it helps a bit. There are auxiliary structures built, per graph, to determine the minimum node cuts. Each node is replaced by 2 nodes, additional directed edges are added, etc. according to the Algorithm 9 in http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
We can reuse these structures instead of reconstructing them inside a tight loop:
Improvement for Case 2:
from networkx.algorithms.connectivity import (
build_auxiliary_node_connectivity)
from networkx.algorithms.flow import build_residual_network
from networkx.algorithms.flow import edmonds_karp
def getone_sided_cuts_Case2(G, A, B):
# build auxiliary networks
H = build_auxiliary_node_connectivity(G)
R = build_residual_network(H, 'capacity')
# get all cutes that consist of nodes exclusively from B which disconnet
# nodes from A
one_sided_cuts = []
seen = []
l = list(combinations(A,2))
for x in l:
s = x[0]
t = x[1]
cut = minimum_st_node_cut(G, s, t, auxiliary=H, residual=R)
if set(cut).issubset(B):
if cut not in seen:
one_sided_cuts.append(cut)
seen.append(cut)
# Find minimum cut size
cur_min = float('inf')
for i in one_sided_cuts:
if len(i) < cur_min:
curr_min = len(i)
one_sided_cuts = [x for x in one_sided_cuts if len(x) == cur_min]
return one_sided_cuts
For profiling purposes, you might use the following, or one of the built-in bipartite graph generators in Networkx:
def create_bipartite_graph(size_m, size_n, num_edges):
G = nx.Graph()
edge_list_0 = list(range(size_m))
edge_list_1 = list(range(size_m,size_m+size_n))
all_edges = []
G.add_nodes_from(edge_list_0, bipartite=0)
G.add_nodes_from(edge_list_1, bipartite=1)
all_edges = list(product(edge_list_0, edge_list_1))
num_all_edges = len(all_edges)
edges = [all_edges[i] for i in random.sample(range(num_all_edges), num_edges)]
G.add_edges_from(edges)
return G, edge_list_0, edge_list_1
Using %timeit, the second version runs about 5-10% faster.
For Case 1, the logic is a little more involved. We need to consider minimal cuts from nodes only inside B. This requires a change to minimum_st_node_cut in the following way. Then replace all occurences of minimum_st_node_cut to rest_minimum_st_node_cut in your solution or the Case 2 solution I gave above, noting that the new function also requires specification of the sets A, B, necessarily:
def rest_build_auxiliary_node_connectivity(G,A,B):
directed = G.is_directed()
H = nx.DiGraph()
for node in A:
H.add_node('%sA' % node, id=node)
H.add_node('%sB' % node, id=node)
H.add_edge('%sA' % node, '%sB' % node, capacity=1)
for node in B:
H.add_node('%sA' % node, id=node)
H.add_node('%sB' % node, id=node)
H.add_edge('%sA' % node, '%sB' % node, capacity=1)
edges = []
for (source, target) in G.edges():
edges.append(('%sB' % source, '%sA' % target))
if not directed:
edges.append(('%sB' % target, '%sA' % source))
H.add_edges_from(edges, capacity=1)
return H
def rest_minimum_st_node_cut(G, A, B, s, t, auxiliary=None, residual=None, flow_func=edmonds_karp):
if auxiliary is None:
H = rest_build_auxiliary_node_connectivity(G, A, B)
else:
H = auxiliary
if G.has_edge(s,t) or G.has_edge(t,s):
return []
kwargs = dict(flow_func=flow_func, residual=residual, auxiliary=H)
for node in [x for x in A if x not in [s,t]]:
edge = ('%sA' % node, '%sB' % node)
num_in_edges = len(H.in_edges(edge[0]))
H[edge[0]][edge[1]]['capacity'] = num_in_edges
edge_cut = minimum_st_edge_cut(H, '%sB' % s, '%sA' % t,**kwargs)
node_cut = set([n for n in [H.nodes[node]['id'] for edge in edge_cut for node in edge] if n not in A])
return node_cut - set([s,t])
We then have, for example:
In [1]: G = nx.Graph()
# A = [0,1,2,3], B = [4,5,6,7]
In [2]: G.add_edges_from([(0,4),(0,5),(1,6),(1,7),(4,1),(5,1),(6,3),(7,3)])
In [3]: minimum_st_node_cut(G, 0, 3)
{1}
In [4]: rest_minimum_st_node_cut(G,A,B,0,3)
{6, 7}
Finally note that the minimum_st_edge_cut() function returns [] if two nodes are adjacent. Sometimes the convention is to return a set of n-1 nodes in this case, all nodes except the source or sink. Anyway, with the empty list convention, and since your original solution to Case 2 loops over node pairs in A, you will likely get [] as a return value for most configurations, unless no nodes in A are adjacent, say.
EDIT
The OP encountered a problem with bipartite graphs for which the sets A, B contained a mix of integers and str types. It looks to me like the build_auxiliary_node_connectivity converts those str nodes to integers causing collisions. I rewrote things above, I think that takes care of it. I don't see anything in the networkx docs about this, so either use all integer nodes or use the rest_build_auxiliary_node_connectivity() thing above.
I have a directed graph in which I want to efficiently find a list of all K-th order neighbors of a node. K-th order neighbors are defined as all nodes which can be reached from the node in question in exactly K hops.
I looked at networkx and the only function relevant was neighbors. However, this just returns the order 1 neighbors. For higher order, we need to iterate to determine the full set. I believe there should be a more efficient way of accessing K-th order neighbors in networkx.
Is there a function which efficiently returns the K-th order neighbors, without incrementally building the set?
EDIT: In case there exist other graph libraries in Python which might be useful here, please do mention those.
You can use:
nx.single_source_shortest_path_length(G, node, cutoff=K)
where G is your graph object.
For NetworkX the best method is probably to build the set of neighbors at each k. You didn't post your code but it seems you probably already have done this:
import networkx as nx
def knbrs(G, start, k):
nbrs = set([start])
for l in range(k):
nbrs = set((nbr for n in nbrs for nbr in G[n]))
return nbrs
if __name__ == '__main__':
G = nx.gnp_random_graph(50,0.1,directed=True)
print(knbrs(G, 0, 3))
Yes,you can get a k-order ego_graph of a node
subgraph = nx.ego_graph(G,node,radius=k)
then neighbors are nodes of the subgraph
neighbors= list(subgraph.nodes())
I had a similar problem, except that I had a digraph, and I need to maintain the edge-attribute dictionary. This mutual-recursion solution keeps the edge-attribute dictionary if you need that.
def neighbors_n(G, root, n):
E = nx.DiGraph()
def n_tree(tree, n_remain):
neighbors_dict = G[tree]
for neighbor, relations in neighbors_dict.iteritems():
E.add_edge(tree, neighbor, rel=relations['rel'])
#you can use this map if you want to retain functional purity
#map(lambda neigh_rel: E.add_edge(tree, neigh_rel[0], rel=neigh_rel[1]['rel']), neighbors_dict.iteritems() )
neighbors = list(neighbors_dict.iterkeys())
n_forest(neighbors, n_remain= (n_remain - 1))
def n_forest(forest, n_remain):
if n_remain <= 0:
return
else:
map(lambda tree: n_tree(tree, n_remain=n_remain), forest)
n_forest( [root] , n)
return E
You solve your problem using modified BFS algorithm. When you're storing node in queue, store it's level (distance from root) as well. When you finish processing the node (all neighbours visited - node marked as black) you can add it to list of nodes of its level. Here is example based on this simple implementation:
#!/usr/bin/python
# -*- coding: utf-8 -*-
from collections import defaultdict
from collections import deque
kth_step = defaultdict(list)
class BFS:
def __init__(self, node,edges, source):
self.node = node
self.edges = edges
self.source = source
self.color=['W' for i in range(0,node)] # W for White
self.graph =color=[[False for i in range(0,node)] for j in range(0,node)]
self.queue = deque()
# Start BFS algorithm
self.construct_graph()
self.bfs_traversal()
def construct_graph(self):
for u,v in self.edges:
self.graph[u][v], self.graph[v][u] = True, True
def bfs_traversal(self):
self.queue.append((self.source, 1))
self.color[self.source] = 'B' # B for Black
kth_step[0].append(self.source)
while len(self.queue):
u, level = self.queue.popleft()
if level > 5: # limit searching there
return
for v in range(0, self.node):
if self.graph[u][v] == True and self.color[v]=='W':
self.color[v]='B'
kth_step[level].append(v)
self.queue.append((v, level+1))
'''
0 -- 1---7
| |
| |
2----3---5---6
|
|
4
'''
node = 8 # 8 nodes from 0 to 7
edges =[(0,1),(1,7),(0,2),(1,3),(2,3),(3,5),(5,6),(2,4)] # bi-directional edge
source = 0 # set fist node (0) as source
bfs = BFS(node, edges, source)
for key, value in kth_step.items():
print key, value
Output:
$ python test.py
0 [0]
1 [1, 2]
2 [3, 7, 4]
3 [5]
4 [6]
I don't know networkx, neither I found ready to use algorithm in Graph Tool. I believe such a problem isn't common enough to have its own function. Also I think it would be overcomplicated, inefficient and redundant to store lists of k-th neighbours for any node in graph instance so such a function would probably have to iterate over nodes anyway.
As proposed previously, the following solution gives you all secondary neighbors (neighbors of neighbors) and lists all neighbors once (the solution is based on BFS):
{n: path for n, path in nx.single_source_shortest_path(G, 'a', cutoff=2).items() if len(path)==3}
Another solution which is slightly faster (6.68 µs ± 191 ns vs. 13.3 µs ± 32.1 ns, measured with timeit) includes that in undirected graphs the neighbor of a neighbor can be the source again:
def k_neighbors(G, source, cutoff):
neighbors = {}
neighbors[0] = {source}
for k in range(1, cutoff+1):
neighbors[k] = set()
for node in level[k-1]:
neighbors[k].update(set(G.neighbors(node)))
return neighbors
k_neighbors(B, 'a', 2) #dict keyed with level until `cutoff`, in this case 2
Both solutions give you the source itself as 0th-order neighbor.
So it depends on your context which one to prefer.