Facebook Pixel

2747. Count Zero Request Servers

Problem Description

You have n servers numbered from 0 to n-1, and you're given a log of requests that these servers received. Each log entry is represented as [server_id, time], indicating that the server with id server_id received a request at time time.

You're also given:

  • An integer x representing a time window size
  • An array queries containing query times

For each query time in queries[i], you need to determine how many servers did not receive any requests during the time interval [queries[i] - x, queries[i]] (inclusive on both ends).

Example breakdown:

  • If you have 3 servers (n=3) and x=5
  • For a query at time 10, you check the interval [5, 10]
  • If only server 0 and server 2 received requests in this interval, then server 1 didn't receive any requests
  • So the answer for this query would be 1

The function should return an array where the i-th element represents the number of inactive servers for the i-th query in the original queries array.

Key points:

  • A server is counted as "inactive" for a query if it received zero requests during that query's time window
  • The time intervals are inclusive (both start and end times are included)
  • The order of results must match the original order of queries
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to efficiently count distinct servers that received requests within different time windows. If we process queries in their original order, we'd need to repeatedly scan through logs for each query, which would be inefficient.

Instead, we can process queries in chronological order (sorted by time). This allows us to use a sliding window approach where we maintain the set of active servers as we move through time.

Think of it like watching a timeline unfold:

  • As time progresses, new requests come into our window (right boundary moves forward)
  • Old requests fall out of our window (left boundary moves forward)
  • We only need to track which servers are currently active in our window

For each query with time r, we need to check the interval [r - x, r]. By sorting queries by their time value, we can:

  1. Maintain two pointers - one for adding new logs that enter the window (k), and one for removing old logs that exit the window (j)
  2. Use a counter/hash map to track how many requests each server has in the current window
  3. When a server's count drops to 0, we know it's no longer active in this window

The beauty of this approach is that each log entry is processed at most twice - once when entering the window and once when leaving. This gives us an efficient solution compared to repeatedly scanning all logs for each query.

Since we process queries out of their original order, we need to keep track of each query's original index to place the results correctly in the final answer array. This is why we use sorted(zip(queries, count())) to pair each query with its original position.

Learn more about Sorting and Sliding Window patterns.

Solution Approach

The solution implements an offline query processing technique with sliding window and two pointers. Here's how it works step by step:

1. Initial Setup:

  • Sort the logs by time: logs.sort(key=lambda x: x[1])
  • Create a Counter to track active servers in the current window: cnt = Counter()
  • Initialize two pointers j = 0 (left boundary) and k = 0 (right boundary)
  • Create result array: ans = [0] * len(queries)

2. Process Queries in Sorted Order:

  • Sort queries by their time value while preserving original indices: sorted(zip(queries, count()))
  • This pairs each query time with its original position: (query_time, original_index)

3. For Each Query (r, i):

  • Calculate window boundaries: left boundary l = r - x, right boundary is r

4. Expand Window (Move Right Pointer):

while k < len(logs) and logs[k][1] <= r:
    cnt[logs[k][0]] += 1
    k += 1
  • Add all logs with time ≤ r to the window
  • Increment the count for each server that received a request

5. Shrink Window (Move Left Pointer):

while j < len(logs) and logs[j][1] < l:
    cnt[logs[j][0]] -= 1
    if cnt[logs[j][0]] == 0:
        cnt.pop(logs[j][0])
    j += 1
  • Remove all logs with time < l from the window
  • Decrement the count for each server
  • If a server's count reaches 0, remove it from the counter entirely

6. Calculate Result:

  • ans[i] = n - len(cnt)
  • The number of inactive servers = total servers - number of servers in the counter
  • Store result at the original index i

Key Data Structures:

  • Counter/HashMap (cnt): Tracks how many requests each server has in the current window
  • Two Pointers (j, k): Maintain sliding window boundaries efficiently
  • Sorted Arrays: Both logs and queries are processed in time-sorted order

Time Complexity: O(m log m + q log q) where m is the number of logs and q is the number of queries Space Complexity: O(n) for the counter in worst case when all servers are active

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Given:

  • n = 3 servers (numbered 0, 1, 2)
  • logs = [[1, 3], [2, 6], [1, 5], [0, 8], [1, 9]]
  • x = 5 (time window size)
  • queries = [10, 6, 4]

Step 1: Sort the logs by time

logs = [[1, 3], [1, 5], [2, 6], [0, 8], [1, 9]]

Step 2: Pair queries with their original indices and sort by time

Original: queries = [10, 6, 4] with indices [0, 1, 2]
Sorted: [(4, 2), (6, 1), (10, 0)]

Step 3: Process each sorted query

Query (4, 2): Time = 4, Original index = 2

  • Window: [4-5, 4] = [-1, 4]
  • Move right pointer k to include logs with time ≤ 4:
    • Add log [1, 3]: cnt = {1: 1}
    • k stops at log [1, 5] (time 5 > 4)
  • Move left pointer j to exclude logs with time < -1:
    • No logs to remove (all times are positive)
  • Active servers: {1}
  • Inactive servers: 3 - 1 = 2 (servers 0 and 2 are inactive)
  • ans[2] = 2

Query (6, 1): Time = 6, Original index = 1

  • Window: [6-5, 6] = [1, 6]
  • Move right pointer k to include logs with time ≤ 6:
    • Add log [1, 5]: cnt = {1: 2}
    • Add log [2, 6]: cnt = {1: 2, 2: 1}
    • k stops at log [0, 8] (time 8 > 6)
  • Move left pointer j to exclude logs with time < 1:
    • No logs to remove (log [1, 3] has time 3 ≥ 1)
  • Active servers: {1, 2}
  • Inactive servers: 3 - 2 = 1 (server 0 is inactive)
  • ans[1] = 1

Query (10, 0): Time = 10, Original index = 0

  • Window: [10-5, 10] = [5, 10]
  • Move right pointer k to include logs with time ≤ 10:
    • Add log [0, 8]: cnt = {1: 2, 2: 1, 0: 1}
    • Add log [1, 9]: cnt = {1: 3, 2: 1, 0: 1}
    • k reaches end of logs
  • Move left pointer j to exclude logs with time < 5:
    • Remove log [1, 3]: cnt = {1: 2, 2: 1, 0: 1}
    • j stops at log [1, 5] (time 5 ≥ 5)
  • Active servers: {0, 1, 2}
  • Inactive servers: 3 - 3 = 0 (all servers are active)
  • ans[0] = 0

Final Result:

ans = [0, 1, 2]

This means:

  • For query at time 10: 0 servers were inactive in interval [5, 10]
  • For query at time 6: 1 server was inactive in interval [1, 6]
  • For query at time 4: 2 servers were inactive in interval [-1, 4]

The key efficiency comes from the sliding window - we process each log entry at most twice (once when entering the window, once when leaving), and we maintain the active server count incrementally rather than recalculating from scratch for each query.

Solution Implementation

1class Solution:
2    def countServers(
3        self, n: int, logs: List[List[int]], x: int, queries: List[int]
4    ) -> List[int]:
5        """
6        Count the number of servers that received no requests within specified time windows.
7      
8        Args:
9            n: Total number of servers (numbered from 1 to n)
10            logs: List of [server_id, time] representing server requests
11            x: Time window size
12            queries: List of query times, for each we check window [query-x, query]
13      
14        Returns:
15            List of integers where each element represents the count of servers 
16            with no requests in the corresponding time window
17        """
18        from collections import Counter
19        from itertools import count
20        from typing import List
21      
22        # Counter to track active servers in current window
23        server_count = Counter()
24      
25        # Sort logs by timestamp for efficient window processing
26        logs.sort(key=lambda log: log[1])
27      
28        # Initialize result array with same length as queries
29        result = [0] * len(queries)
30      
31        # Two pointers for sliding window technique
32        left_pointer = 0   # Points to the start of the current window
33        right_pointer = 0  # Points to the end of the current window
34      
35        # Process queries in sorted order while maintaining original indices
36        # zip(queries, count()) pairs each query with its original index
37        for query_time, original_index in sorted(zip(queries, count())):
38            # Calculate window boundaries
39            window_start = query_time - x
40            window_end = query_time
41          
42            # Expand window right: add all logs with timestamp <= window_end
43            while right_pointer < len(logs) and logs[right_pointer][1] <= window_end:
44                server_id = logs[right_pointer][0]
45                server_count[server_id] += 1
46                right_pointer += 1
47          
48            # Shrink window left: remove all logs with timestamp < window_start
49            while left_pointer < len(logs) and logs[left_pointer][1] < window_start:
50                server_id = logs[left_pointer][0]
51                server_count[server_id] -= 1
52                # Remove server from counter if count reaches 0
53                if server_count[server_id] == 0:
54                    server_count.pop(server_id)
55                left_pointer += 1
56          
57            # Calculate servers with no requests (total servers - active servers)
58            result[original_index] = n - len(server_count)
59      
60        return result
61
1class Solution {
2    public int[] countServers(int n, int[][] logs, int x, int[] queries) {
3        // Sort logs by timestamp in ascending order
4        Arrays.sort(logs, (a, b) -> a[1] - b[1]);
5      
6        int queryCount = queries.length;
7      
8        // Create array of [queryValue, originalIndex] pairs to maintain original query order
9        int[][] queriesWithIndex = new int[queryCount][2];
10        for (int i = 0; i < queryCount; ++i) {
11            queriesWithIndex[i] = new int[] {queries[i], i};
12        }
13      
14        // Sort queries by value in ascending order
15        Arrays.sort(queriesWithIndex, (a, b) -> a[0] - b[0]);
16      
17        // Map to track count of requests per server within current window
18        Map<Integer, Integer> serverRequestCount = new HashMap<>();
19      
20        // Result array to store answers
21        int[] result = new int[queryCount];
22      
23        // Two pointers for sliding window
24        int leftPointer = 0;  // Points to start of window
25        int rightPointer = 0; // Points to end of window
26      
27        // Process each query in sorted order
28        for (int[] currentQuery : queriesWithIndex) {
29            int rightBound = currentQuery[0];
30            int originalIndex = currentQuery[1];
31            int leftBound = rightBound - x;
32          
33            // Add all logs with timestamp <= rightBound to the window
34            while (rightPointer < logs.length && logs[rightPointer][1] <= rightBound) {
35                int serverId = logs[rightPointer][0];
36                serverRequestCount.merge(serverId, 1, Integer::sum);
37                rightPointer++;
38            }
39          
40            // Remove all logs with timestamp < leftBound from the window
41            while (leftPointer < logs.length && logs[leftPointer][1] < leftBound) {
42                int serverId = logs[leftPointer][0];
43                // Decrement count and remove server if count becomes 0
44                if (serverRequestCount.merge(serverId, -1, Integer::sum) == 0) {
45                    serverRequestCount.remove(serverId);
46                }
47                leftPointer++;
48            }
49          
50            // Calculate servers with no requests in window [leftBound, rightBound]
51            // Total servers - servers that received at least one request
52            result[originalIndex] = n - serverRequestCount.size();
53        }
54      
55        return result;
56    }
57}
58
1class Solution {
2public:
3    vector<int> countServers(int n, vector<vector<int>>& logs, int x, vector<int>& queries) {
4        // Sort logs by timestamp in ascending order
5        sort(logs.begin(), logs.end(), [](const auto& a, const auto& b) {
6            return a[1] < b[1];
7        });
8      
9        int numQueries = queries.size();
10      
11        // Create pairs of (query_value, original_index) to maintain original order after sorting
12        vector<pair<int, int>> queryPairs(numQueries);
13        for (int i = 0; i < numQueries; ++i) {
14            queryPairs[i] = {queries[i], i};
15        }
16      
17        // Sort queries by value in ascending order
18        sort(queryPairs.begin(), queryPairs.end());
19      
20        // Map to count requests per server within the time window
21        unordered_map<int, int> serverRequestCount;
22      
23        // Result array to store answers in original query order
24        vector<int> result(numQueries);
25      
26        // Two pointers for sliding window
27        int leftPointer = 0;   // Points to the start of the time window
28        int rightPointer = 0;  // Points to the end of the time window
29      
30        // Process each query in sorted order
31        for (auto& [queryTime, originalIndex] : queryPairs) {
32            // Calculate the left boundary of the time window [queryTime - x, queryTime]
33            int windowStart = queryTime - x;
34          
35            // Add all logs with timestamp <= queryTime to the window
36            while (rightPointer < logs.size() && logs[rightPointer][1] <= queryTime) {
37                int serverId = logs[rightPointer][0];
38                ++serverRequestCount[serverId];
39                ++rightPointer;
40            }
41          
42            // Remove all logs with timestamp < windowStart from the window
43            while (leftPointer < logs.size() && logs[leftPointer][1] < windowStart) {
44                int serverId = logs[leftPointer][0];
45                --serverRequestCount[serverId];
46              
47                // Remove server from map if it has no requests in the window
48                if (serverRequestCount[serverId] == 0) {
49                    serverRequestCount.erase(serverId);
50                }
51                ++leftPointer;
52            }
53          
54            // Calculate number of servers with no requests in the time window
55            // Total servers - servers with at least one request
56            result[originalIndex] = n - serverRequestCount.size();
57        }
58      
59        return result;
60    }
61};
62
1/**
2 * Counts the number of servers that received no requests during specified time windows
3 * @param n - Total number of servers (numbered from 1 to n)
4 * @param logs - Array of [serverId, time] pairs representing server requests
5 * @param x - Time window duration
6 * @param queries - Array of query times to check
7 * @returns Array containing count of servers with no requests for each query
8 */
9function countServers(n: number, logs: number[][], x: number, queries: number[]): number[] {
10    // Sort logs by timestamp in ascending order
11    logs.sort((a: number[], b: number[]) => a[1] - b[1]);
12  
13    const queryCount: number = queries.length;
14  
15    // Create array of [queryTime, originalIndex] pairs to maintain original order after sorting
16    const queriesWithIndex: number[][] = [];
17    for (let i = 0; i < queryCount; ++i) {
18        queriesWithIndex.push([queries[i], i]);
19    }
20  
21    // Sort queries by time in ascending order
22    queriesWithIndex.sort((a: number[], b: number[]) => a[0] - b[0]);
23  
24    // Map to track count of requests per server in current window
25    const serverRequestCount: Map<number, number> = new Map();
26  
27    // Initialize result array
28    const result: number[] = new Array(queryCount);
29  
30    // Pointer for start of time window
31    let windowStart: number = 0;
32    // Pointer for end of time window
33    let windowEnd: number = 0;
34  
35    // Process each query in sorted order
36    for (const [queryTime, originalIndex] of queriesWithIndex) {
37        // Calculate the start time of the window [queryTime - x, queryTime]
38        const windowStartTime: number = queryTime - x;
39      
40        // Add all logs that fall within the window end time
41        while (windowEnd < logs.length && logs[windowEnd][1] <= queryTime) {
42            const serverId: number = logs[windowEnd][0];
43            serverRequestCount.set(serverId, (serverRequestCount.get(serverId) || 0) + 1);
44            ++windowEnd;
45        }
46      
47        // Remove all logs that fall before the window start time
48        while (windowStart < logs.length && logs[windowStart][1] < windowStartTime) {
49            const serverId: number = logs[windowStart][0];
50            const currentCount: number = (serverRequestCount.get(serverId) || 0) - 1;
51          
52            if (currentCount === 0) {
53                // Remove server from map if no requests remain in window
54                serverRequestCount.delete(serverId);
55            } else {
56                serverRequestCount.set(serverId, currentCount);
57            }
58            ++windowStart;
59        }
60      
61        // Calculate servers with no requests: total servers - servers with requests
62        result[originalIndex] = n - serverRequestCount.size;
63    }
64  
65    return result;
66}
67

Time and Space Complexity

Time Complexity: O(l × log l + m × log m + l + m) which simplifies to O(l × log l + m × log m) where l is the length of the logs array and m is the length of the queries array.

The analysis breaks down as follows:

  • Sorting logs by timestamp: O(l × log l)
  • Sorting queries with their indices using sorted(zip(queries, count())): O(m × log m)
  • Processing each query: O(m) iterations
    • The inner while loops for expanding and shrinking the window iterate through each log at most once across all queries: O(l) total
  • Total: O(l × log l + m × log m + l + m) = O(l × log l + m × log m)

Space Complexity: O(l + m) where l is the length of the logs array and m is the length of the queries array.

The space usage includes:

  • The cnt Counter dictionary which can store at most n server IDs (where nl since each log corresponds to a server): O(n) or O(l) in worst case
  • The sorted queries with indices (from sorted(zip(queries, count()))): O(m)
  • The ans array to store results: O(m)
  • Total: O(l + m)

Note: The reference answer includes n in the time complexity formula, but n only appears in the computation n - len(cnt) which is O(1) per query, so it doesn't affect the overall asymptotic complexity.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Window Boundary Handling

A critical pitfall is misunderstanding the inclusive nature of the time window [queries[i] - x, queries[i]]. Developers often make mistakes with the boundary conditions when moving the pointers.

Incorrect Implementation:

# Wrong: Using <= for left boundary removal
while left_pointer < len(logs) and logs[left_pointer][1] <= window_start:
    server_count[logs[left_pointer][0]] -= 1
    # ... rest of removal logic

Why it's wrong: This removes logs that are exactly at window_start, but the window is inclusive on both ends. Logs at time window_start should remain in the window.

Correct Implementation:

# Correct: Using < for left boundary removal
while left_pointer < len(logs) and logs[left_pointer][1] < window_start:
    server_count[logs[left_pointer][0]] -= 1
    # ... rest of removal logic

2. Server Numbering Confusion

Another common mistake is confusion about server numbering. The problem states servers are numbered from 0 to n-1, but developers might assume they're numbered from 1 to n.

Incorrect Assumption:

# Wrong: Assuming servers are 1-indexed
total_servers = n + 1  # This would be wrong
# Or checking if server_id is valid
if server_id < 1 or server_id > n:  # Wrong bounds check

Correct Understanding:

  • Servers are numbered: 0, 1, 2, ..., n-1
  • Total number of servers is exactly n
  • Valid server IDs range from 0 to n-1

3. Processing Queries in Wrong Order

A subtle but critical error is forgetting to restore the original query order after sorting.

Incorrect Implementation:

# Wrong: Sorting queries but losing original order
queries.sort()
result = []
for query_time in queries:
    # Process query
    result.append(inactive_servers)
return result  # Results are in sorted query order, not original order!

Correct Implementation:

# Correct: Maintain original indices while sorting
for query_time, original_index in sorted(zip(queries, count())):
    # Process query
    result[original_index] = inactive_servers  # Store at original position

4. Counter Cleanup Memory Leak

Failing to remove servers with zero count from the Counter can lead to incorrect results and unnecessary memory usage.

Incorrect Implementation:

# Wrong: Only decrementing without cleanup
while left_pointer < len(logs) and logs[left_pointer][1] < window_start:
    server_count[logs[left_pointer][0]] -= 1
    left_pointer += 1
    # Missing: removal of zero-count entries

Why it's wrong: Servers with count 0 would still be counted in len(server_count), leading to incorrect inactive server calculations.

Correct Implementation:

# Correct: Remove servers with zero count
while left_pointer < len(logs) and logs[left_pointer][1] < window_start:
    server_id = logs[left_pointer][0]
    server_count[server_id] -= 1
    if server_count[server_id] == 0:
        server_count.pop(server_id)  # Critical: remove from counter
    left_pointer += 1

5. Edge Case: Empty Logs or Queries

Not handling edge cases where logs or queries might be empty.

Solution:

# Add early returns for edge cases
if not queries:
    return []
if not logs:
    return [n] * len(queries)  # All servers inactive for all queries
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More