Degrees of Separation Between Members

Updated · By

Run a breadth-first search from both members at once, always expanding the smaller frontier, and stop as soon as the frontiers meet or the two depths add up to three. Searching from both ends explores about b + b² members instead of b³ for b connections each. Return 1, 2 or 3 hops, and -1 for anything further.

hardPattern: GraphsFrom Devana's LinkedIn question set

Problem

A profile page shows whether the viewer is a 1st, 2nd or 3rd degree connection of the member they are looking at. It runs on every profile view, over a graph where some members have tens of thousands of connections.

Write connection_degree(graph, member_a, member_b), where graph maps each member to their connections, which are mutual. Return the number of hops on the shortest path between the two members if it is 1, 2 or 3, and -1 if they are further apart or not connected at all.

def connection_degree(graph: dict[str, list[str]], member_a: str, member_b: str) -> int:

This is one of Devana's own questions, written in the style of LinkedIn's interviews. It is not a question leaked from LinkedIn, and Devana is not affiliated with LinkedIn.

Constraints

  • Connections are mutual: if B is in A's list, A is in B's.
  • member_a and member_b are different members.
  • Return 1, 2 or 3 for the hop count, and -1 for anything further or unreachable.
  • The search must stop at depth 3 rather than exploring the whole network.
  • A member can have up to 30,000 connections.

Examples

Example 1

Input:  graph = {"A": ["C"], "C": ["A", "B"], "B": ["C"]}, member_a = "A", member_b = "B"
Output: 2

One member, C, in between.

Example 2

Input:  a path A, B, C, D, E; member_a = "A", member_b = "E"
Output: -1

Four hops is beyond three degrees, the same answer as no path at all, which is why the search can stop early.

Hints

Open one at a time, and try the problem again after each.

Hint 1

Shortest path in an unweighted graph is breadth-first search. How many members can a BFS touch by depth three if each has hundreds of connections?

Hint 2

Distance three means a path A, x, y, B. Could you find x from A's side and y from B's side and check whether they meet?

Hint 3

Grow a frontier from each end and always expand the smaller one. A neighbor that the other side has already reached completes a path.

Hint 4

Track the depth each side has reached, and stop when the two add up to three.

Approach: from brute force to optimal

BFS from one side

Expand A's connections level by level until B turns up or depth three is exhausted. Correct, but the third level is enormous: with 500 connections each, that is up to 500³, about 125 million members touched for one profile view.

Complexity: O(b³) for b connections per member

Bidirectional BFS with a depth cap

Keep a distance map for each side. Expand whichever frontier is smaller by one level; for each neighbor the other side has already reached, the path length is this side's depth plus the other side's distance to it. Finish the level and return the shortest length found. Stop when the depths add up to three.

For b = 500, depth three becomes one level from one side and two from the other: about 250,000 members instead of 125 million.

Complexity: O(b + b²) members touched; O(b²) space

How to say it in the interview

Say BFS first, then do the arithmetic for depth three on a dense graph out loud; that number is what justifies searching from both ends. Explain the meeting check and the stopping rule, and bring up the member with 30,000 connections as the case that makes expanding the smaller frontier matter.

Solution

Python

def connection_degree(graph, member_a, member_b, max_degree=3):
    dist = ({member_a: 0}, {member_b: 0})      # distance from each end
    frontier = [[member_a], [member_b]]
    depth = [0, 0]
    while frontier[0] and frontier[1] and depth[0] + depth[1] < max_degree:
        side = 0 if len(frontier[0]) <= len(frontier[1]) else 1   # the smaller one
        mine, other = dist[side], dist[1 - side]
        depth[side] += 1
        best, grown = None, []
        for member in frontier[side]:
            for friend in graph.get(member, []):
                if friend in other:                # the two searches meet
                    hops = depth[side] + other[friend]
                    best = hops if best is None else min(best, hops)
                if friend not in mine:
                    mine[friend] = depth[side]
                    grown.append(friend)
        if best is not None:
            return best
        frontier[side] = grown
    return -1

JavaScript

function connectionDegree(graph, memberA, memberB, maxDegree = 3) {
  const dist = [new Map([[memberA, 0]]), new Map([[memberB, 0]])]; // distance from each end
  const frontier = [[memberA], [memberB]];
  const depth = [0, 0];
  while (frontier[0].length > 0 && frontier[1].length > 0 && depth[0] + depth[1] < maxDegree) {
    const side = frontier[0].length <= frontier[1].length ? 0 : 1; // the smaller one
    const mine = dist[side];
    const other = dist[1 - side];
    depth[side] += 1;
    let best = null;
    const grown = [];
    for (const member of frontier[side]) {
      for (const friend of graph[member] ?? []) {
        if (other.has(friend)) { // the two searches meet
          const hops = depth[side] + other.get(friend);
          best = best === null ? hops : Math.min(best, hops);
        }
        if (!mine.has(friend)) {
          mine.set(friend, depth[side]);
          grown.push(friend);
        }
      }
    }
    if (best !== null) return best;
    frontier[side] = grown;
  }
  return -1;
}

Both versions are tested against the same cases, including every example on this page.

  • Expanding the smaller frontier is what protects you from the member with 30,000 connections: their side is expanded last, if at all.
  • The whole level finishes before returning, so the shortest of several meetings at that level is the one reported.
  • max_degree is a parameter, so the same code answers a cheaper "within two" check.

Follow-up questions

Why does a bidirectional search matter so much here?

Search cost grows exponentially with depth. Splitting depth three into one level from one side and two from the other replaces b³ with about b², which for b in the hundreds is the difference between a hundred million members and a few hundred thousand.

What does a member with 30,000 connections do to your traversal?

Expanding them alone is 30,000 lookups. Expanding the smaller frontier usually avoids it; when it cannot, cap the work per request and fall back to a precomputed answer, or treat very well connected members specially.

How would you shard a graph this large across machines?

Partition the adjacency lists by member id. Each BFS level becomes a batch of lookups sent to the shards that own those members, so the cost is in round trips, which the depth cap bounds.

How do you keep the answer correct as connections are added continuously?

Compute it on read from the live adjacency lists, as here, rather than storing degrees. If you cache second degree sets for heavy users, expire them whenever their first degree list changes.

Common mistakes

  • A single-sided BFS to depth three on a dense graph.
  • Returning 4 or more instead of -1.
  • Returning at the first meeting inside a level instead of finishing the level, which can report a longer path than the shortest.
  • Exploring the whole connected component to prove that two members are far apart.

Practice it in a mock interview

Reading a solution and explaining one under questioning are different skills. The button below starts this problem as a voice mock interview with Josh, Devana's AI interviewer, with an editor that runs your code.

Practice this problem in a mock interview

Practice this problem out loud

Josh, Devana's AI interviewer, runs this exact problem as a voice mock interview, asks follow-ups like the ones above, and scores you out of 100. The Free plan has 3 interviews a month, no card needed.

Practice this problem in a mock interview