Speeding up this recursive procedure in python - python

Given a graph H, I have to find all graphs g that fulfill a special requirement (which I call requirement2). All these graphs are stored in a set s. The graphs g must have the same number of vertices as H and all graphs are directed. So in essence, I am finding edges that I can add to g to satisfy requirement2. I notate my graphs using a dictionary so for example:
H = {
'1': {'1': set([(0, 1)])},
'3': {'3': set([(0, 1)]), '2': set([(0, 1)])},
'2': {'1': set([(0, 1)]), '3': set([(0, 1)]), '2': set([(0, 1)])},
}
means that there is an edge from vertex 1 to 1 and an edge from 3 to 2 etc...Please ignore the set([(0, 1)])'s for now for they are not relevant to my question. To find these g's that satisfy requirement2, I add an edge to an empty graph g with the same number of vertices as H and then use requirement1 to get an array of edges that I know might potentially work for satisfying requirement2. Then I add one of those edges deemed ok by requirement1 to g and test requirement2. Why I do not directly use requirement2 is that it would be way too time consuming to add every edge that g does not have and then test requirement2 on it. Early pruning saves time for me. I proceed with a Depth first search and if requirement 1 returns an empty array, I know I have to backtrack. The current code I have uses memoization to store subproblem solutions but this code is still not fast enough...With 7 nodes, this code can take 2500 seconds to run. Any suggestions on how to speed this up would be appreciated.
A little more on some functions this code uses:
addanedge(g,e) adds edge e to g
delanedge(g,e) deletes edge e in g
edgelist(g) returns a list of edges of g
complement(g) returns a list of edges that g does not have
requirement1(H,g) returns T if g fulfills some requirement
involving H and F otherwise. It's purpose is for pruning down the number of edges I need to add for each round.
requirement2(H,g) returns T if g fulfills some requirement involving H and F otherwise. If true, I add g to s
memo stores my subproblem solutions
gsig simply converts the graph g to a form that I can add to a set.
def findgs(H):
g = {n:{} for n in H}
s = set()
#memo
def addedges(g,H,edges):
if edges:
masks = []
for e in edges:
addanedge(g,e)
if requirement1(H,g):
masks.append(False)
else:
masks.append(True)
delanedge(g,e)
nedges = [edges[i] for i in range(len(edges)) if masks[i]]
n = len(nedges)
if n:
for i in range(n):
addanedge(g,nedges[i])
if requirement2(H,g):
s.add(gsig(g))
addedges(g,H,nedges[:i]+nedges[i+1:])
delanedge(g,nedges[i])
edges = edgelist(complement(g))
addedges(g,H,edges)
return s
def memo(func):
cache = {}
#wraps(func)
def wrap(*args):
s = tool.signature(args[0],args[2])
if s not in cache:
cache[s] = func(*args)
return cache[s]
return wrap
One thing I have noticed is that the same graphs that satisfy requirement2 appear MANY times and are cut out because s is a set and duplicates are not allowed.

Related

Fastest way of checking if a subgraph is a clique in NetworkX

I want to find out if a given subgraph of G is a complete graph. I was expecting to find a built in function, like is_complete_graph(G), but I can't see anything like that.
My current solution is to create a new helper function:
def is_complete(G):
n = G.order()
return n*(n-1)/2 == G.size()
I imagine this is probably fast but I feel wrong implementing this kind of thing myself, and I feel there must be a 'right' way to do it in NetworkX.
I only need a solution for simple undirected graphs.
edit
The answer at the bottom is relatively clean. However, it appears that the following is faster:
def is_subclique(G,nodelist):
H = G.subgraph(nodelist)
n = len(nodelist)
return H.size() == n*(n-1)/2
I have to admit, I don't entirely understand. But clearly creating the subgraph is faster than checking whether each edge exists.
Slower alternative I expected to be faster:
We'll check to see if all of the edges are there. We'll use combinations to generate the pairs we check. Note that if combinations returns (1,2), then it will not return (2,1).
from itertools import combinations
import networkx as nx
def is_subclique(G, nodelist):
r'''For each pair of nodes in nodelist whether there is an edge
if any edge is missing, we know that it's not a subclique.
if all edges are there, it is a subclique
'''
for (u,v) in combinations(nodelist,2): #check each possible pair
if not G.has_edge(u,v):
return False #if any edge is missing we're done
return True #if we get to here, then every edge was there. It's True.
G = nx.Graph()
G.add_edges_from([(1,2), (2,3), (3,1), (4,1)])
is_subclique(G, [1,2,3])
> True
is_subclique(G, [1,4])
> True
is_subclique(G, [2,3,4])
> False
You actually need to check more than the number of edges because selfloops aren't allowed for a complete_graph. Also, they potentially change the expected count of edges.
Here's a very fast function -- especially if it isn't a complete graph. Notice that it avoids even counting the edges. It just makes sure each node has the right neighbors.
def is_complete_graph(G):
N = len(G) - 1
return not any(n in nbrdict or len(nbrdict)!=N for n, nbrdict in G.adj.items())

Kruskels MST algorithm in python seems to be not quite right

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.

Given edges in a undirected graph, what is an algorithm for limiting the maximum degree of the graph while maximizing the degree of the graph?

This is for my research in protein folding (So I guess technically a school project)
Summary:
I have the edges of an weighted undirected graph. Each vertex of the graph has anywhere from 1 to 20-ish edges. I would like to trim this graph down such that no vertex has more than 6 edges. I would also like the graph to retain as much connectivity as possible (maximize the degree).
Background:
I have a Delaunay Tesselation of the atoms (pointcloud essentially) in a protein using the scipy library. I use this to create a list of all pairs of residues that are in contact with each other (I store the distance between them). This list contains every pair (twice), and the distance between the pairs. (The residue contains many atoms so I use the average position of them to get the position of the residue)
pairs
[(ALA 1, GLU 2, 2.7432), (ALA 1, GLU 2, 2.7432), (ALA 4, ASP 27, 4.8938), (ALA 4, ASP 27, 4.8938) ... ]
What I have tried (which works but isn't exactly what I want) is to only store the six closest contacts. (I sort the residue names so I can use collections later)
for contact in residue.contacts[:6]:
pairs.append( tuple( sorted([residue.name, contact.name], key=lambda r: r.name) + [residue.dist[contact]] ) )
And then remove any contacts that are not reciprocated. (I guess technically add contacts that are)
new_pairs = []
counter=collections.Counter(pairs)
for key, val in counter.items():
if val == 2:
new_pairs.append(key)
This works, but I lose some information that I would like to keep. I phrased the question as a graph theory problem because I feel like this problem has already been solved in that field.
I was thinking that greedy algorithm might work:
while run_greedy:
# find the residue with the maximum number of neighbors
# find that residues pair with the maximum number of neighbors but only if the pair exists in pairs
# remove that pair from pairs
# if maximum_degree <= 6: run_greedy = False
Does the greedy algorithm work? Are there known algorithms that do this well? Is there a library that can do this (I am more than willing to change the format of the data to fit the library)?
I hope this is enough information, Thanks in advance for the help.
EDIT this is an variant of the knapsack problem: you add edges one by one, and want to maximize the number of edges while the graph built doesn't exceed a given degree.
The following solution uses dynamic programming.
Let m[i, d] the maximum subset of edges in e_0, ..., e_{i-1} creating a subgraph of maximium degree <= d.
m[i, 0] = {}
m[0, d] = {}
m[i, d] = m[i-1, d] + {e_i} if the degree of the graph is <= d
m[i, d] = m[i-1, d-1] + {e_i} if it has more edges than m[i-1][d], else m[i-1][d].
Hence the algorithm (not tested):
for i in 0..N:
m[i][0] = {}
for d in 1..K:
m[0][d] = {}
for d in 1..K:
for i in 1..N:
G1 = m[i-1][d] + {e_i}
if D(G1) == d: # can add e_i with degree <= k
m[i][d] = G1
else:
m[i][d] = max(m[i-1][d-1] + {e_i}, m[i-1][d]) # key=cardinal
Solution is: m[N-1][K-1]. Time complexity is O(K N^2) (imbricated loops : K N + maximum degre of the graph in N or less)
Previous answer
TLDR; I don't know how to find an optimal solution, but a greedy algorithm might give you acceptable result.
The problem
Let me rephrase the problem, based on your question and your code: you want to remove a minimum number of edges from your graph in order to reduce the maximum degree the graph to 6. That is to get the maximal subgraph G' from G with D(u) <= 6 for all u in G'.
The closest idea I found is the K-core of a graph, but that's not exactly the same problem.
Your method
Your method is clearly not optimal, since you keep at most 6 edges of every vertex and recreate the graph with those edges. Take the graph A-B-C:
A -> 1. B, 2. C
B -> 1. C, 2. A
C -> 1. A, 2. B
If you try to reduce the maximum degree of this graph to 1 using your method, the first pass will remove A-B (B is the 2nd neighbor of A), B-A (A is the 2nd neighbor of B) and C-B (B is the 2nd neighbor of C):
A -> 1. B
B -> 1. C
C -> 1. A
The second pass, to insure that the graph is undirected, will remove all the remaining edges (and vertices).
An optimal reduction would be:
A -> 1. B
B -> 1. A
Or any other pair of vertices in A, B, C.
Some strategy
Let:
k = 6
D(u) = max(d(u)-k, 0): the number of neighbors above k, or 0
w(u-v) (resp s(u-v)) = the weak (resp. strong) endpoint of the edge: having the lowest (resp. highest) degree
m(u-v) = min(D(u), D(v))
M(u-v) = max(D(u), D(v))
Let S = sum(D(u) for u in G). The goal is to make S = 0 while removing a minimum number of edges. If you remove:
(1) a floating edge: m(u-v) > 0, then S is decreased by 2 (both endpoints loose 1 degree)
(2) a sinking edge: m(u-v) = 0 and M(u-v) > 0, then S is decreased by 1 (the degree of the weak endpoint is already <= 6)
(3) a sunk edge: M(u-v) = 0, then S is unchanged
Note that a floating edge may become a sinking edge if: 1. its weak endpoint has a degree of k+1; 2. you remove another edge connected to this endpoint. Similarly, a sinking edge can sunk.
You have to remove floating edges while avoid creating sinking edges, because removing a floating edges is more efficient to reduce S. Let K the number of floating edges removed, and L the number of sinking edges removed (we don't remove sunk edges) to make S = 0. We want 2*K + L >= S. Obviously, the idea is to make L as small a possible, because we want a small number of edges removed (K + L).
I doubt you'll find an optimal greedy algorithm, because everything depends on the order of removing and the remote consequences of the current removing are hard to predict.
But you can use a general strategy to limit the creation of sinking edges:
do not remove edges with m(u-v) = 1 unless you have no choice.
if you have to remove an edge with m(u-v) = 1, choose the one whose weak endpoint has the less floating edges (they will become sinking edges).
An algorithm
Here's a greedy algorithm that implements this strategy:
while {u, v in G | m(u-v) > 0} is not empty: // remove floating edges first
remove the edge u-v with:
1. the maxmimum m(u-v)
2. w(u-v) has the minimum of neighbors t with D(t) > 0
3. s(u-v) has the minimum of neighbors t with D(t) > 0
remove all edges from {u, v in G | M(u-v) > 0} // clean up sinking edges
clean orphan vertices
Termination the algorithm terminates because we remove an edge on each iteration, thus {u in G | D(u) > 0} will become empty at some point.
Note: you can use a heap and update m(u-v) after each removing.

Networkx: Find all minimal cuts consisting of only nodes from one set in a bipartite graph

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.

K-th order neighbors in graph - Python networkx

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.

Categories