1311. Get Watched Videos by Your Friends
Problem Description
You have a social network with n
people, where each person has a unique ID from 0
to n-1
. You're given two arrays:
watchedVideos[i]
: list of videos watched by person withid = i
friends[i]
: list of friend IDs for person withid = i
The problem asks you to find videos watched by people at a specific "friendship distance" from you.
Understanding Friendship Levels:
- Level 1: Your direct friends
- Level 2: Friends of your friends (but not your direct friends)
- Level k: People exactly
k
steps away from you in the friendship network
Task:
Given your id
and a target level
, you need to:
- Find all people at exactly that friendship distance from you
- Collect all videos watched by those people
- Count how many times each video appears
- Return the videos sorted by:
- Frequency (ascending) - less frequent videos come first
- If frequencies are equal, sort alphabetically (A to Z)
Example: If you're person 0 and want level 2 videos:
- First find all your direct friends (level 1)
- Then find their friends who aren't already in your visited set (level 2)
- Collect and count all videos watched by these level 2 friends
- Sort and return the result
The solution uses BFS to traverse the friendship graph level by level, ensuring we only collect videos from people at the exact specified distance.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a social network where people (nodes) are connected through friendships (edges). We have
friends[i]
representing the adjacency list for personi
.
Is it a tree?
- No: The friendship network is not a tree - it can have cycles (mutual friendships) and is undirected. A person can have multiple friends, and friendship paths can loop back.
Is the problem related to directed acyclic graphs?
- No: The friendship graph is undirected (if A is friends with B, then B is friends with A), and it can contain cycles.
Is the problem related to shortest paths?
- Yes: We need to find all people at exactly a specific distance (
level
) from the starting person. This is essentially finding all nodes at a specific shortest path distance.
Is the graph Weighted?
- No: All friendships have the same "weight" or distance of 1. Each friendship connection counts as one step in the path.
BFS
- Yes: Since we have an unweighted graph and need to find nodes at a specific distance, BFS is the appropriate algorithm.
Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for this problem. BFS is perfect here because:
- It explores nodes level by level
- It guarantees finding the shortest path in unweighted graphs
- It allows us to stop exactly at the desired friendship level
- It naturally groups all nodes at the same distance together
Intuition
The key insight is recognizing that this is a level-based graph traversal problem. When we think about "friendship distance," we're essentially asking: "Who can I reach by making exactly k
friendship hops?"
Imagine standing in the center of concentric circles. Your immediate friends form the first circle around you. Their friends (who aren't already in previous circles) form the second circle, and so on. We want to collect videos from people standing in a specific circle.
Why BFS works perfectly here:
-
Level-by-level exploration: BFS naturally processes nodes in "waves" or levels. When we start from person
id
, BFS first visits all friends at distance 1, then all friends at distance 2, and so on. -
Avoiding revisits: As we expand outward, we need to mark people as visited to avoid counting them multiple times. Someone might be friends with multiple people in our network, but we only want to reach them through the shortest path.
-
Exact distance control: Unlike DFS which might reach a person through a longer path first, BFS guarantees we reach everyone via their shortest path. By tracking how many levels we've traversed, we can stop exactly at the desired level.
The video counting part is straightforward once we identify the target people. We simply:
- Collect all videos watched by people at the target level
- Count frequencies using a hash map
- Sort by the given criteria (frequency first, then alphabetically)
The elegance of this solution lies in using BFS's natural property of level-wise traversal to precisely identify people at the exact friendship distance we need.
Learn more about Breadth-First Search, Graph and Sorting patterns.
Solution Approach
The implementation follows the BFS pattern we identified, with careful attention to level control and video counting.
Step 1: Initialize BFS Setup
- Create a queue
q
with the starting personid
- Use a set
vis
to track visited people, initially containing justid
- These structures ensure we explore each person exactly once
Step 2: Level-wise Traversal
We perform exactly level
iterations to reach the target friendship distance:
for _ in range(level):
for _ in range(len(q)):
i = q.popleft()
for j in friends[i]:
if j not in vis:
vis.add(j)
q.append(j)
The outer loop runs level
times, each iteration representing one friendship hop. The inner loop processes all people at the current level before moving to the next. This two-loop structure is crucial:
- The inner
for _ in range(len(q))
ensures we process only the people at the current level - We dequeue each person
i
and enqueue their unvisited friendsj
- The
vis
set prevents cycles and repeated visits
Step 3: Count Videos at Target Level
After level
iterations, the queue contains exactly the people at distance level
:
cnt = Counter() for i in q: for v in watchedVideos[i]: cnt[v] += 1
We use Python's Counter
to efficiently count video frequencies. For each person in the queue, we iterate through their watched videos and increment the count.
Step 4: Sort and Return Results
return sorted(cnt.keys(), key=lambda k: (cnt[k], k))
The sorting uses a custom key that creates tuples (frequency, video_name)
. Python's sort is stable and handles the two-tier sorting requirement:
- Primary sort by frequency (ascending)
- Secondary sort by video name (alphabetical) when frequencies match
Time Complexity: O(N + E)
for BFS traversal where N is the number of people reached and E is the friendship connections explored, plus O(V log V)
for sorting videos where V is the unique video count.
Space Complexity: O(N)
for the queue and visited set, plus O(V)
for the video counter.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Setup:
- 4 people in the network (IDs: 0, 1, 2, 3)
watchedVideos = [["A","B"], ["C"], ["B","C"], ["D"]]
friends = [[1,2], [0,3], [0,3], [1,2]]
- We start from
id = 0
and want videos atlevel = 2
Visualization of the friendship network:
0 (watches: A,B) / \ 1 2 (watches: C) and (B,C) \ / 3 (watches: D)
Step 1: Initialize BFS
- Queue:
[0]
- Visited:
{0}
Step 2: Level-wise Traversal
Level 1 iteration (finding direct friends):
- Process person 0 from queue
- Person 0's friends are [1, 2]
- Add unvisited friends to queue and mark as visited
- After Level 1: Queue =
[1, 2]
, Visited ={0, 1, 2}
Level 2 iteration (finding friends of friends):
- Process person 1 from queue
- Person 1's friends are [0, 3]
- 0 is already visited, so only add 3
- Process person 2 from queue
- Person 2's friends are [0, 3]
- Both 0 and 3 are already visited (3 was just added)
- After Level 2: Queue =
[3]
, Visited ={0, 1, 2, 3}
Step 3: Count Videos at Target Level
- Queue now contains people at exactly distance 2:
[3]
- Person 3 watched videos: ["D"]
- Video count: {"D": 1}
Step 4: Sort and Return
- Only one video "D" with frequency 1
- Return:
["D"]
Let's trace through a more complex scenario where sorting matters:
Modified example:
If person 3 had watched ["D", "A", "A", "B"]
instead:
- Video count: {"D": 1, "A": 2, "B": 1}
- Sorting by (frequency, name):
- ("B", 1) comes before ("D", 1) alphabetically
- ("A", 2) has highest frequency
- Return:
["B", "D", "A"]
This example demonstrates how BFS naturally finds people at the exact friendship distance and how the two-tier sorting organizes the final video list.
Solution Implementation
1from collections import deque, Counter
2from typing import List
3
4class Solution:
5 def watchedVideosByFriends(
6 self,
7 watchedVideos: List[List[str]],
8 friends: List[List[int]],
9 id: int,
10 level: int,
11 ) -> List[str]:
12 """
13 Find videos watched by friends at a specific friendship level.
14
15 Args:
16 watchedVideos: List where watchedVideos[i] contains videos watched by person i
17 friends: List where friends[i] contains the friend IDs of person i
18 id: Starting person's ID
19 level: The friendship level to search (1 = direct friends, 2 = friends of friends, etc.)
20
21 Returns:
22 List of videos sorted by frequency (ascending) and then alphabetically
23 """
24 # Initialize BFS queue with starting person
25 queue = deque([id])
26 # Track visited people to avoid cycles
27 visited = {id}
28
29 # Perform BFS to reach the target friendship level
30 for _ in range(level):
31 # Process all people at current level
32 current_level_size = len(queue)
33 for _ in range(current_level_size):
34 current_person = queue.popleft()
35 # Add unvisited friends to the queue for next level
36 for friend_id in friends[current_person]:
37 if friend_id not in visited:
38 visited.add(friend_id)
39 queue.append(friend_id)
40
41 # Count videos watched by all people at the target level
42 video_count = Counter()
43 for person_id in queue:
44 for video in watchedVideos[person_id]:
45 video_count[video] += 1
46
47 # Sort videos by frequency (ascending), then alphabetically
48 return sorted(video_count.keys(), key=lambda video: (video_count[video], video))
49
1class Solution {
2 public List<String> watchedVideosByFriends(
3 List<List<String>> watchedVideos, int[][] friends, int id, int level) {
4
5 // Initialize BFS queue and add starting person
6 Deque<Integer> queue = new ArrayDeque<>();
7 queue.offer(id);
8
9 // Track visited people to avoid cycles
10 int totalPeople = friends.length;
11 boolean[] visited = new boolean[totalPeople];
12 visited[id] = true;
13
14 // Perform BFS to find all friends at exactly 'level' distance
15 while (level > 0) {
16 int currentLevelSize = queue.size();
17
18 // Process all people at current level
19 for (int i = 0; i < currentLevelSize; i++) {
20 int currentPerson = queue.poll();
21
22 // Add all unvisited friends of current person to queue
23 for (int friend : friends[currentPerson]) {
24 if (!visited[friend]) {
25 visited[friend] = true;
26 queue.offer(friend);
27 }
28 }
29 }
30 level--;
31 }
32
33 // Count frequency of videos watched by friends at target level
34 Map<String, Integer> videoFrequency = new HashMap<>();
35 for (int person : queue) {
36 for (String video : watchedVideos.get(person)) {
37 videoFrequency.merge(video, 1, Integer::sum);
38 }
39 }
40
41 // Create result list from all unique videos
42 List<String> result = new ArrayList<>(videoFrequency.keySet());
43
44 // Sort by frequency (ascending), then alphabetically for ties
45 result.sort((video1, video2) -> {
46 int frequency1 = videoFrequency.get(video1);
47 int frequency2 = videoFrequency.get(video2);
48
49 if (frequency1 == frequency2) {
50 return video1.compareTo(video2); // Alphabetical order for same frequency
51 }
52 return Integer.compare(frequency1, frequency2); // Ascending frequency order
53 });
54
55 return result;
56 }
57}
58
1class Solution {
2public:
3 vector<string> watchedVideosByFriends(vector<vector<string>>& watchedVideos, vector<vector<int>>& friends, int id, int level) {
4 // Initialize BFS queue with starting person
5 queue<int> bfsQueue{{id}};
6 int totalPeople = friends.size();
7
8 // Track visited people to avoid cycles
9 vector<bool> visited(totalPeople);
10 visited[id] = true;
11
12 // Perform BFS to reach friends at exactly 'level' distance
13 while (level--) {
14 int currentLevelSize = bfsQueue.size();
15
16 // Process all people at current level
17 for (int i = 0; i < currentLevelSize; ++i) {
18 int currentPerson = bfsQueue.front();
19 bfsQueue.pop();
20
21 // Add unvisited friends to next level
22 for (int friendId : friends[currentPerson]) {
23 if (!visited[friendId]) {
24 visited[friendId] = true;
25 bfsQueue.push(friendId);
26 }
27 }
28 }
29 }
30
31 // Count videos watched by friends at target level
32 unordered_map<string, int> videoFrequency;
33 while (!bfsQueue.empty()) {
34 int personAtTargetLevel = bfsQueue.front();
35 bfsQueue.pop();
36
37 // Count each video watched by this person
38 for (const auto& video : watchedVideos[personAtTargetLevel]) {
39 videoFrequency[video]++;
40 }
41 }
42
43 // Collect all unique videos
44 vector<string> result;
45 for (const auto& [video, frequency] : videoFrequency) {
46 result.push_back(video);
47 }
48
49 // Sort videos by frequency (ascending), then alphabetically
50 sort(result.begin(), result.end(), [&videoFrequency](const string& videoA, const string& videoB) {
51 if (videoFrequency[videoA] == videoFrequency[videoB]) {
52 return videoA < videoB; // Alphabetical order if same frequency
53 }
54 return videoFrequency[videoA] < videoFrequency[videoB]; // Ascending frequency order
55 });
56
57 return result;
58 }
59};
60
1/**
2 * Finds videos watched by friends at a specific level from a given user
3 * @param watchedVideos - 2D array where watchedVideos[i] contains videos watched by user i
4 * @param friends - 2D array where friends[i] contains the friend list of user i
5 * @param id - Starting user ID
6 * @param level - The level of friends to consider (1 = direct friends, 2 = friends of friends, etc.)
7 * @returns Array of video names sorted by frequency (ascending) and alphabetically for ties
8 */
9function watchedVideosByFriends(
10 watchedVideos: string[][],
11 friends: number[][],
12 id: number,
13 level: number,
14): string[] {
15 // Initialize BFS queue with starting user
16 let currentLevelQueue: number[] = [id];
17 const totalUsers: number = friends.length;
18
19 // Track visited users to avoid cycles
20 const visitedUsers: boolean[] = Array(totalUsers).fill(false);
21 visitedUsers[id] = true;
22
23 // Perform BFS to find friends at the target level
24 while (level-- > 0) {
25 const nextLevelQueue: number[] = [];
26
27 // Process all users at current level
28 for (const currentUser of currentLevelQueue) {
29 // Add unvisited friends to next level
30 for (const friendId of friends[currentUser]) {
31 if (!visitedUsers[friendId]) {
32 visitedUsers[friendId] = true;
33 nextLevelQueue.push(friendId);
34 }
35 }
36 }
37
38 // Move to next level
39 currentLevelQueue = nextLevelQueue;
40 }
41
42 // Count video frequencies from target level friends
43 const videoFrequencyMap: { [videoName: string]: number } = {};
44
45 for (const userId of currentLevelQueue) {
46 for (const videoName of watchedVideos[userId]) {
47 videoFrequencyMap[videoName] = (videoFrequencyMap[videoName] || 0) + 1;
48 }
49 }
50
51 // Get all unique video names
52 const resultVideos: string[] = Object.keys(videoFrequencyMap);
53
54 // Sort by frequency (ascending), then alphabetically for ties
55 resultVideos.sort((videoA: string, videoB: string) => {
56 if (videoFrequencyMap[videoA] === videoFrequencyMap[videoB]) {
57 return videoA.localeCompare(videoB);
58 }
59 return videoFrequencyMap[videoA] - videoFrequencyMap[videoB];
60 });
61
62 return resultVideos;
63}
64
Time and Space Complexity
Time Complexity: O(n + m + v Γ log v)
The time complexity breaks down as follows:
- The BFS traversal visits each person at most once and explores each friendship edge at most once, taking
O(n + m)
time, wheren
is the number of people andm
is the total number of friendship edges. - After reaching the target level, we iterate through all friends at that level and count their watched videos, which takes
O(v)
time wherev
is the total number of videos watched by friends at the target level. - The final sorting operation sorts the unique videos by their frequency and lexicographically, which takes
O(v Γ log v)
time in the worst case. - Overall:
O(n + m + v Γ log v)
Space Complexity: O(n + v)
The space complexity consists of:
- The
vis
set stores visited person IDs, which can contain at mostn
elements:O(n)
- The queue
q
stores person IDs and contains at mostn
elements at any point:O(n)
- The
cnt
Counter stores video frequencies, containing at mostv
unique videos:O(v)
- Overall:
O(n + v)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Processing Beyond the Target Level
One of the most common mistakes is accidentally including people from level k+1
when asked for level k
. This happens when the BFS traversal isn't properly bounded.
Incorrect approach:
visited = {id}
queue = deque([id])
current_level = 0
while queue and current_level < level:
current_level += 1
for _ in range(len(queue)):
person = queue.popleft()
for friend in friends[person]:
if friend not in visited:
visited.add(friend)
queue.append(friend)
# Bug: Queue might contain level k+1 people!
for person in queue:
# This processes the wrong level
process_videos(person)
Why it fails: After the last iteration, the queue contains people at level k+1
, not level k
.
Solution: Either track people at each level separately or ensure the queue contains exactly the target level after traversal completes (as shown in the correct implementation).
2. Including Already Visited People in Video Count
Another pitfall is forgetting to exclude the starting person or previously visited people from the target level, leading to incorrect video counts.
Incorrect approach:
# Missing visited set initialization
queue = deque([id])
for _ in range(level):
new_level = []
for person in queue:
for friend in friends[person]:
new_level.append(friend) # No duplicate check!
queue = deque(new_level)
Why it fails: If person A and person B are both friends with person C, person C gets added twice, doubling their video counts.
Solution: Always maintain a visited
set and check membership before adding to the queue.
3. Incorrect Sorting Logic
The problem requires sorting by frequency first (ascending), then alphabetically. A common mistake is reversing this order or using descending frequency.
Incorrect approaches:
# Wrong: Alphabetical first, then frequency
return sorted(video_count.keys(), key=lambda v: (v, video_count[v]))
# Wrong: Descending frequency
return sorted(video_count.keys(), key=lambda v: (-video_count[v], v))
# Wrong: Not handling the secondary sort
return sorted(video_count.keys(), key=lambda v: video_count[v])
Solution: Use the correct tuple order in the sort key: (video_count[v], v)
for ascending frequency, then alphabetical.
4. Edge Case: Level 0
When level = 0
, the answer should be videos watched by the starting person themselves, not their friends.
Incorrect handling:
if level == 0: return [] # Wrong!
Correct handling:
# The main algorithm handles this correctly: # After 0 iterations, queue still contains only [id] # So we return videos watched by the starting person
5. Modifying the Queue During Iteration
Using the queue's current length dynamically can cause issues:
Incorrect approach:
for _ in range(level):
# Bug: len(queue) changes during iteration!
for i in range(len(queue)):
if i >= len(queue): # Might go out of bounds
break
person = queue.popleft()
# ...
Solution: Capture the queue length before the inner loop starts: current_size = len(queue)
, then iterate exactly current_size
times.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Donβt Miss This!