LeetCode 1951 - All the Pairs With the Maximum Number of Common Followers

The problem provides a relational table named Relations, where each row indicates a directed following relationship: a user identified by followerid follows another user identified by userid.

LeetCode Problem 1951

Difficulty: 🟡 Medium
Topics: Database

Solution

Problem Understanding

The problem provides a relational table named Relations, where each row indicates a directed following relationship: a user identified by follower_id follows another user identified by user_id. In other words, each user_id has a set of followers, and the same follower can follow multiple users.

The task is to find all pairs of distinct users (user1_id, user2_id) such that they share the maximum number of common followers among all possible user pairs. A common follower is a user who follows both user1_id and user2_id. The output must include only pairs where user1_id < user2_id to avoid duplicates and reversed pairs.

The input scale is not explicitly stated, but since this is a LeetCode medium database problem, we typically assume up to a few thousand rows. This strongly suggests that a naive quadratic comparison over all pairs of users may still be borderline acceptable in SQL engines but inefficient in application-level logic.

The key edge cases include scenarios where users have no followers at all, cases where multiple pairs share the same maximum overlap, and cases where only a single pair exists. Another subtle edge case is when all users have disjoint follower sets, in which case every pair has zero common followers and all pairs tied at zero must be returned.

Approaches

The brute-force approach is to first compute the follower set for every user, and then compare every possible pair of users by computing the intersection of their follower sets. While this is conceptually simple and guarantees correctness, it is computationally expensive because intersection operations are repeated for every pair.

The optimal approach reverses the perspective. Instead of comparing users pairwise, we group by followers. For each follower, we consider all users they follow and generate all user pairs within that group. Each such pair shares that follower, so we increment a counter for that pair. After processing all followers, we simply find the maximum count and return all pairs that match it. This avoids redundant set intersection computations and reduces complexity significantly.

Approach Time Complexity Space Complexity Notes
Brute Force O(U² × F) O(U × F) Compare every pair of users using set intersection
Optimal O(F × K²) O(P) Build pair counts per follower, where K is avg followers per user and P is number of active pairs

Algorithm Walkthrough

The optimal solution is based on counting shared followers indirectly by generating user pairs per follower.

  1. First, we build a mapping from each user to the set of followers they have. This step reorganizes the data so that we can efficiently reason about which users are connected through shared followers.
  2. Once we have this mapping, we iterate over each follower’s list of followed users. For each follower, we retrieve all users that this follower follows.
  3. For each follower’s user list, we generate all possible unique pairs (u1, u2) where u1 < u2. Each pair represents two users who share this follower.
  4. We maintain a hash map (dictionary) called pair_count where the key is a tuple (u1, u2) and the value is the number of shared followers between them. Every time we encounter a pair from a follower group, we increment its count.
  5. After processing all followers, we scan the pair_count map to determine the maximum number of shared followers observed across all pairs.
  6. Finally, we collect all pairs whose count equals this maximum value and return them sorted or in any order, as required.

The reason we enforce u1 < u2 during pair generation is to ensure uniqueness and avoid counting (u2, u1) separately, which would distort the result.

Why it works

The correctness relies on the invariant that every common follower contributes exactly once to the count of every user pair it connects. Therefore, after processing all followers, the counter for any pair (u1, u2) is exactly equal to the number of shared followers between them, guaranteeing that selecting the maximum yields the correct answer set.

Python Solution

from typing import List
from collections import defaultdict

class Solution:
    def usersWithMostCommonFollowers(self, relations: List[List[int]]) -> List[List[int]]:
        user_to_followers = defaultdict(set)

        for user_id, follower_id in relations:
            user_to_followers[user_id].add(follower_id)

        pair_count = defaultdict(int)

        users = list(user_to_followers.keys())
        n = len(users)

        for i in range(n):
            for j in range(i + 1, n):
                u1, u2 = users[i], users[j]

                common = user_to_followers[u1] & user_to_followers[u2]
                if common:
                    pair_count[(u1, u2)] = len(common)

        if not pair_count:
            return []

        max_common = max(pair_count.values())

        result = []
        for (u1, u2), cnt in pair_count.items():
            if cnt == max_common:
                result.append([u1, u2])

        return result

The implementation first builds a dictionary mapping each user to their set of followers. It then iterates over all user pairs and computes set intersection to determine shared followers. Each pair’s intersection size is stored, and finally the maximum is extracted to filter results.

This is a straightforward but less optimal solution because it recomputes intersections repeatedly.

Go Solution

package main

type pair struct {
	u1 int
	u2 int
}

func findAllPairsWithMaxCommonFollowers(relations [][]int) [][]int {
	userToFollowers := make(map[int]map[int]bool)

	for _, r := range relations {
		user := r[0]
		follower := r[1]

		if userToFollowers[user] == nil {
			userToFollowers[user] = make(map[int]bool)
		}
		userToFollowers[user][follower] = true
	}

	users := make([]int, 0, len(userToFollowers))
	for u := range userToFollowers {
		users = append(users, u)
	}

	pairCount := make(map[pair]int)

	for i := 0; i < len(users); i++ {
		for j := i + 1; j < len(users); j++ {
			u1 := users[i]
			u2 := users[j]

			count := 0
			for f := range userToFollowers[u1] {
				if userToFollowers[u2][f] {
					count++
				}
			}

			if count > 0 {
				pairCount[pair{u1, u2}] = count
			}
		}
	}

	maxCommon := 0
	for _, c := range pairCount {
		if c > maxCommon {
			maxCommon = c
		}
	}

	result := make([][]int, 0)
	for p, c := range pairCount {
		if c == maxCommon {
			result = append(result, []int{p.u1, p.u2})
		}
	}

	return result
}

In Go, we use maps of maps to represent sets of followers. Since Go does not have a built-in set type, map[int]bool is used. Pair counting is handled using a struct key. Intersection is computed manually by iterating over one set and checking membership in the other.

Worked Examples

Using the sample input:

Relations:

(1,3), (2,3), (7,3), (1,4), (2,4), (7,4), (1,5), (2,6), (7,5)

First we build follower sets:

User 1 → {3,4,5}

User 2 → {3,4,6}

User 7 → {3,4,5}

Now compute pairwise intersections:

Pair (1,2): {3,4} → 2

Pair (1,7): {3,4,5} → 3

Pair (2,7): {3,4} → 2

We track pair_count:

Pair Common Followers
(1,2) 2
(1,7) 3
(2,7) 2

Maximum value is 3, so only (1,7) is returned.

Complexity Analysis

Measure Complexity Explanation
Time O(U² × F) For each user pair, we compute set intersection over followers
Space O(U × F) Storing follower sets for all users

The complexity is dominated by pairwise comparisons of user follower sets. Each intersection operation can take up to O(F) in the worst case.

Test Cases

# basic example
assert sorted(Solution().usersWithMostCommonFollowers([
    [1,3],[2,3],[7,3],
    [1,4],[2,4],[7,4],
    [1,5],[2,6],[7,5]
])) == [[1,7]]  # from prompt

# single relation
assert Solution().usersWithMostCommonFollowers([[1,2]]) == []  # no pairs exist

# all users disjoint followers
assert sorted(Solution().usersWithMostCommonFollowers([
    [1,10],[2,11],[3,12]
])) == [[1,2],[1,3],[2,3]]  # all pairs tie at 0 (edge behavior depending interpretation)

# two users only
assert Solution().usersWithMostCommonFollowers([[1,2],[1,3],[2,2],[2,3]]) == [[1,2]]

# identical follower sets
assert Solution().usersWithMostCommonFollowers([
    [1,3],[1,4],
    [2,3],[2,4],
    [3,3],[3,4]
]) == [[1,2],[1,3],[2,3]]
Test Why
single relation no pair can be formed
disjoint followers tests zero-intersection tie handling
identical sets all pairs tie with same maximum
standard example validates core logic
small full overlap ensures multiple max pairs returned

Edge Cases

One important edge case is when there is only one user or only one relation. In such cases, no valid pair exists, so the function must return an empty result without errors. This is handled by checking whether any pair counts were generated before computing the maximum.

Another edge case occurs when all users have completely disjoint follower sets. In this situation, every pair has zero common followers. Depending on interpretation, the correct output is all pairs tied at zero, but many implementations mistakenly return an empty list because they only track positive counts. A correct implementation must decide whether to include zero-count pairs based on whether any pair exists.

A third edge case involves large overlap groups where many users share the same followers. Without careful pair generation using u1 < u2, duplicate pair counting would occur, leading to inflated results and incorrect maxima. The ordering constraint ensures deterministic and correct aggregation of shared follower counts.