3017. Count the Number of Houses at a Certain Distance II
Problem Description
You have a city with n
houses numbered from 1
to n
. These houses are connected by streets in the following way:
- There's a street connecting each consecutive pair of houses: house
i
connects to housei + 1
for all1 ≤ i ≤ n - 1
- There's one additional street that directly connects house
x
to housey
This creates a graph where houses form a linear path from 1
to n
, with one extra connection creating a potential shortcut between houses x
and y
.
Your task is to find, for each distance k
from 1
to n
, how many pairs of houses (house₁, house₂)
have exactly k
as the minimum number of streets needed to travel between them.
The output should be an array result
of length n
where:
result[k]
= the total number of house pairs that are exactlyk
streets apart (using the shortest path)- The array is 1-indexed, meaning
result[1]
represents pairs that are 1 street apart,result[2]
represents pairs that are 2 streets apart, and so on
Important notes:
x
andy
can be equal (meaning the extra street connects a house to itself, which doesn't add any new path)- Each pair
(house₁, house₂)
wherehouse₁ ≠ house₂
should be counted in the result - The distance is measured as the minimum number of streets to traverse, considering both the linear connections and the extra street between
x
andy
For example, if n = 4
, x = 1
, y = 3
, the houses are connected as: 1-2-3-4
with an extra street 1-3
. The shortest distances between all pairs would need to be calculated considering both the direct path and any path using the shortcut.
Intuition
The key insight is that the extra street between houses x
and y
transforms part of the linear path into a cycle. Without this extra street, we'd have a simple line graph where the distance between any two houses i
and j
is just |i - j|
. But with the extra street, we create a cycle that can serve as a shortcut.
Let's think about what happens when we add the edge between x
and y
:
- If
|x - y| ≤ 1
, the extra street doesn't create any shortcuts (it either connects a house to itself or connects two already adjacent houses), so we maintain the linear structure. - If
|x - y| > 1
, we form a cycle of length|x - y| + 1
(the houses fromx
toy
plus the direct edge back).
The graph now consists of three parts:
- A "tail" before the cycle: houses
1
tox-1
- The cycle itself: houses from
x
toy
- Another "tail" after the cycle: houses
y+1
ton
For any pair of houses, the shortest path might:
- Stay entirely within one tail (using the linear path)
- Go from one tail into the cycle
- Go through the cycle to reach the other tail
- Stay within the cycle (where we can go either clockwise or counterclockwise)
The solution breaks down the problem by:
- First handling the case without the cycle (when
|x - y| ≤ 1
) - Computing distances as if we had a shorter linear graph of length
n - cycle_len + 2
(collapsing the cycle into just 2 nodes) - Adding the contribution from pairs within the cycle itself
- Adjusting for the interactions between each tail and the cycle
The cycle creates interesting distance patterns. For example, in a cycle of even length, opposite nodes are equidistant going either direction, while in odd-length cycles, all pairs have a unique shortest path direction. This is why the solution treats even and odd cycle lengths differently with conditions like cycle_len & 1
.
The approach systematically counts pairs at each distance by considering these three components and their interactions, building up the final count array by adding contributions from each part of the structure.
Learn more about Graph and Prefix Sum patterns.
Solution Approach
The implementation handles three distinct cases based on the structure created by the extra street:
Case 1: No Effective Cycle (|x - y| ≤ 1
)
When the distance between x
and y
is at most 1, the extra street doesn't create any shortcuts. The graph remains effectively linear, so for each distance k
, there are exactly n - k
pairs of houses that are k
streets apart. Since we count both (i, j)
and (j, i)
, we multiply by 2:
if abs(x - y) <= 1:
return [2 * x for x in reversed(range(n))]
This gives us [2*(n-1), 2*(n-2), ..., 2*1, 0]
.
Case 2: Graph with Cycle
When |x - y| > 1
, we form a cycle and decompose the problem:
Step 1: Collapse the cycle
cycle_len = abs(x - y) + 1
n2 = n - cycle_len + 2
res = [2 * x for x in reversed(range(n2))]
We treat the cycle as if it were just 2 nodes, creating a linear graph of length n2
. This accounts for distances in the "collapsed" version.
Step 2: Add cycle internal distances
res2 = [cycle_len * 2] * (cycle_len >> 1) if not cycle_len & 1: # Even cycle res2[-1] = cycle_len res2[0] -= 2
- For a cycle, pairs at distance
d
(whered ≤ cycle_len/2
) appear with specific frequencies - In even-length cycles, antipodal pairs (at distance
cycle_len/2
) have both paths equal, so we count them once - We subtract 2 from
res2[0]
to avoid double-counting the cycle's entry/exit points
Step 3: Handle tail-to-cycle interactions
tail1 = x - 1 # Left tail length tail2 = n - y # Right tail length
For each tail, we calculate additional distances created by paths that go from the tail into the cycle:
i_mx
: Maximum distance achievable from this tail through the cycleval_mx
: Maximum contribution value based on cycle and tail sizes- The pattern fills distances based on how far into the cycle we can reach
The algorithm builds res3
array for each tail:
res3 = [val_mx] * i_mx
# Set initial values
res3[0] = 0
res3[1] = 0
# Fill pattern based on cycle properties
for i, j in enumerate(range(4, val_mx, 4)):
res3[i + 2] = j
res3[i_mx2 - i - 1] = j
Step 4: Adjust for direct tail connections
for i in range(1, tail + 1):
res3[i] += 2
This adds the direct connections within each tail.
Step 5: Handle even cycle special case
if not cycle_len & 1:
mn = cycle_len >> 1
for i in range(mn, mn + tail):
res3[i] += 2
For even-length cycles, additional adjustments are needed for distances involving the midpoint of the cycle.
Final Assembly
The solution accumulates all contributions:
- Base distances from the collapsed graph
- Internal cycle distances
- Tail-to-cycle interaction distances for both tails
The result is a complete count of all pairs at each distance from 1 to n
, properly accounting for the shortcut created by the extra street between houses x
and y
.
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 concrete example with n = 6
, x = 2
, y = 5
.
Step 1: Visualize the graph
Original linear path: 1 - 2 - 3 - 4 - 5 - 6 Extra street: 2 --- 5 Final graph: 1 - 2 - 3 - 4 - 5 - 6 └───────────┘
This creates a cycle of length 4 (houses 2, 3, 4, 5).
Step 2: Identify the structure
- Left tail: house 1 (length = 1)
- Cycle: houses 2, 3, 4, 5 (length = 4)
- Right tail: house 6 (length = 1)
Step 3: Calculate base distances (collapsed graph)
Collapse the cycle into 2 nodes: n2 = 6 - 4 + 2 = 4
Base array for linear graph of length 4:
res = [2*3, 2*2, 2*1, 0] = [6, 4, 2, 0]
Step 4: Add cycle internal distances For the 4-cycle, distances between pairs:
- 2↔3: 1, 3↔4: 1, 4↔5: 1, 5↔2: 1 (4 pairs at distance 1)
- 2↔4: 2, 3↔5: 2 (2 pairs at distance 2)
res2 = [4*2, 4/2] = [8, 2]
res2[0] -= 2 = [6, 2] // Adjust for entry/exit points
Step 5: Handle tail interactions
For left tail (house 1):
- 1→2: distance 1 (direct)
- 1→3: distance 2 (via 2)
- 1→4: distance 2 (via 2→5→4, using shortcut)
- 1→5: distance 1 (via 2→5, using shortcut)
For right tail (house 6):
- 6→5: distance 1 (direct)
- 6→4: distance 2 (via 5)
- 6→3: distance 2 (via 5→2→3, using shortcut)
- 6→2: distance 1 (via 5→2, using shortcut)
Step 6: Compute final distances Let's verify all pairs and their shortest distances:
Pair | Direct Path | Using Shortcut | Min Distance |
---|---|---|---|
1↔2 | 1 | - | 1 |
1↔3 | 2 | 2 | 2 |
1↔4 | 3 | 2 (1→2→5→4) | 2 |
1↔5 | 4 | 1 (1→2→5) | 1 |
1↔6 | 5 | 2 (1→2→5→6) | 2 |
2↔3 | 1 | 3 | 1 |
2↔4 | 2 | 2 | 2 |
2↔5 | 3 | 1 (shortcut) | 1 |
2↔6 | 4 | 2 (2→5→6) | 2 |
3↔4 | 1 | 3 | 1 |
3↔5 | 2 | 2 | 2 |
3↔6 | 3 | 3 | 3 |
4↔5 | 1 | 3 | 1 |
4↔6 | 2 | 2 | 2 |
5↔6 | 1 | - | 1 |
Final count by distance:
- Distance 1: pairs (1,2), (1,5), (2,3), (2,5), (3,4), (4,5), (5,6) = 7 pairs × 2 = 14
- Distance 2: pairs (1,3), (1,4), (1,6), (2,4), (2,6), (3,5), (4,6) = 7 pairs × 2 = 14
- Distance 3: pairs (3,6) = 1 pair × 2 = 2
- Distance 4, 5, 6: 0 pairs
Result: [14, 14, 2, 0, 0, 0]
The algorithm efficiently computes this by:
- Starting with the collapsed graph contribution
- Adding cycle-internal pair counts
- Adjusting for paths from tails through the cycle
- Accounting for the shortcut's impact on all reachable pairs
Solution Implementation
1class Solution:
2 def countOfPairs(self, n: int, x: int, y: int) -> List[int]:
3 # If x and y are adjacent or the same, no special edge effect
4 if abs(x - y) <= 1:
5 # Simple chain graph: each distance k appears 2*(n-k) times
6 return [2 * distance for distance in reversed(range(n))]
7
8 # Calculate the cycle length formed by the special edge
9 cycle_length = abs(x - y) + 1
10
11 # Number of nodes after contracting the cycle to a single supernode
12 contracted_nodes = n - cycle_length + 2
13
14 # Initialize result with distances in the contracted graph
15 result = [2 * distance for distance in reversed(range(contracted_nodes))]
16
17 # Pad with zeros to reach n elements
18 while len(result) < n:
19 result.append(0)
20
21 # Calculate contribution from pairs within the cycle
22 cycle_pairs = cycle_length // 2
23 cycle_contribution = [cycle_length * 2] * cycle_pairs
24
25 # Handle even cycle length case
26 if cycle_length % 2 == 0:
27 cycle_contribution[-1] = cycle_length
28
29 # Adjust for endpoints
30 cycle_contribution[0] -= 2
31
32 # Add cycle contributions to result
33 for i in range(len(cycle_contribution)):
34 result[i] += cycle_contribution[i]
35
36 # Ensure x < y for consistent processing
37 if x > y:
38 x, y = y, x
39
40 # Calculate tail lengths (nodes outside the cycle)
41 left_tail_length = x - 1
42 right_tail_length = n - y
43
44 # Process each tail
45 for tail_length in (left_tail_length, right_tail_length):
46 if tail_length == 0:
47 continue
48
49 # Maximum index affected by this tail
50 max_index = tail_length + (cycle_length // 2)
51
52 # Maximum value contribution
53 max_value = 4 * min((cycle_length - 3) // 2, tail_length)
54
55 # Secondary maximum index for even cycle case
56 max_index_2 = max_index - (1 - (cycle_length % 2))
57
58 # Initialize tail contribution array
59 tail_contribution = [max_value] * max_index
60 tail_contribution[0] = 0
61 tail_contribution[1] = 0
62
63 # Special case for even cycle length
64 if cycle_length % 2 == 0:
65 tail_contribution[-1] = 0
66
67 # Build symmetric pattern of contributions
68 for i, value in enumerate(range(4, max_value, 4)):
69 tail_contribution[i + 2] = value
70 tail_contribution[max_index_2 - i - 1] = value
71
72 # Add linear contributions from tail nodes
73 for i in range(1, tail_length + 1):
74 tail_contribution[i] += 2
75
76 # Handle even cycle length special case
77 if cycle_length % 2 == 0:
78 min_distance = cycle_length // 2
79 for i in range(min_distance, min_distance + tail_length):
80 tail_contribution[i] += 2
81
82 # Add tail contributions to final result
83 for i in range(len(tail_contribution)):
84 result[i] += tail_contribution[i]
85
86 return result
87
1class Solution {
2 public long[] countOfPairs(int n, int x, int y) {
3 // Convert to 0-indexed for easier calculation
4 x--;
5 y--;
6
7 // Ensure x <= y for consistency
8 if (x > y) {
9 int temp = x;
10 x = y;
11 y = temp;
12 }
13
14 // Difference array to track distance counts
15 // diff[i] represents the change in count at distance i
16 long[] differenceArray = new long[n];
17
18 // Process each node as a starting point
19 for (int currentNode = 0; currentNode < n; ++currentNode) {
20 // Add 2 for bidirectional counting (each pair counted from both ends)
21 differenceArray[0] += 2;
22
23 // Update distances considering direct path vs path through x-y edge
24 // Path to x directly or through y
25 int distanceToX = Math.min(
26 Math.abs(currentNode - x),
27 Math.abs(currentNode - y) + 1
28 );
29 differenceArray[distanceToX]++;
30
31 // Path to y directly or through x
32 int distanceToY = Math.min(
33 Math.abs(currentNode - y),
34 Math.abs(currentNode - x) + 1
35 );
36 differenceArray[distanceToY]++;
37
38 // Subtract overcounted paths to leftmost node (0)
39 int distanceToStart = Math.min(
40 Math.abs(currentNode - 0),
41 Math.abs(currentNode - y) + 1 + Math.abs(x - 0)
42 );
43 differenceArray[distanceToStart]--;
44
45 // Subtract overcounted paths to rightmost node (n-1)
46 int distanceToEnd = Math.min(
47 Math.abs(currentNode - (n - 1)),
48 Math.abs(currentNode - x) + 1 + Math.abs(y - (n - 1))
49 );
50 differenceArray[distanceToEnd]--;
51
52 // Handle middle section between x and y
53 // Calculate distance for nodes in the middle of x-y segment
54 int leftGap = Math.max(x - currentNode, 0);
55 int rightGap = Math.max(currentNode - y, 0);
56 int segmentLength = y - x;
57
58 // Adjust for even distribution of middle segment
59 differenceArray[leftGap + rightGap + segmentLength / 2]--;
60 differenceArray[leftGap + rightGap + (segmentLength + 1) / 2]--;
61 }
62
63 // Convert difference array to actual counts using prefix sum
64 for (int distance = 0; distance + 1 < n; ++distance) {
65 differenceArray[distance + 1] += differenceArray[distance];
66 }
67
68 return differenceArray;
69 }
70}
71
1class Solution {
2public:
3 vector<long long> countOfPairs(int n, int x, int y) {
4 // Convert to 0-indexed (assuming input is 1-indexed)
5 --x;
6 --y;
7
8 // Ensure x <= y for consistency
9 if (x > y) {
10 swap(x, y);
11 }
12
13 // Difference array to track distance contributions
14 // diff[i] represents the change in count at distance i
15 vector<long long> diff(n);
16
17 // Process each node as a starting point
18 for (int i = 0; i < n; ++i) {
19 // Add 2 for distance 0 (each node contributes to itself twice for bidirectional counting)
20 diff[0] += 2;
21
22 // Add contributions for paths using the direct edge or the shortcut edge (x,y)
23 // These represent different routing options through the graph
24
25 // Path options through x or y with the shortcut
26 ++diff[min(abs(i - x), abs(i - y) + 1)];
27 ++diff[min(abs(i - y), abs(i - x) + 1)];
28
29 // Remove overcounted paths to boundaries (node 0 and node n-1)
30 // This accounts for paths that go through the shortcut unnecessarily
31 --diff[min(abs(i - 0), abs(i - y) + 1 + abs(x - 0))];
32 --diff[min(abs(i - (n - 1)), abs(i - x) + 1 + abs(y - (n - 1)))];
33
34 // Handle the middle section between x and y
35 // Remove contributions for distances within the shortcut region
36 int leftDistance = max(x - i, 0); // Distance to x from left side
37 int rightDistance = max(i - y, 0); // Distance to y from right side
38 int shortcutLength = y - x;
39
40 // Remove overcounted distances for the midpoint regions
41 --diff[leftDistance + rightDistance + shortcutLength / 2];
42 --diff[leftDistance + rightDistance + (shortcutLength + 1) / 2];
43 }
44
45 // Convert difference array to actual counts using prefix sum
46 for (int i = 0; i + 1 < n; ++i) {
47 diff[i + 1] += diff[i];
48 }
49
50 return diff;
51 }
52};
53
1function countOfPairs(n: number, x: number, y: number): number[] {
2 // Convert to 0-indexed (assuming input is 1-indexed)
3 x--;
4 y--;
5
6 // Ensure x <= y for consistency
7 if (x > y) {
8 [x, y] = [y, x];
9 }
10
11 // Difference array to track distance contributions
12 // diff[i] represents the change in count at distance i
13 const diff: number[] = new Array(n).fill(0);
14
15 // Process each node as a starting point
16 for (let i = 0; i < n; i++) {
17 // Add 2 for distance 0 (each node contributes to itself twice for bidirectional counting)
18 diff[0] += 2;
19
20 // Add contributions for paths using the direct edge or the shortcut edge (x,y)
21 // These represent different routing options through the graph
22
23 // Path options through x or y with the shortcut
24 diff[Math.min(Math.abs(i - x), Math.abs(i - y) + 1)]++;
25 diff[Math.min(Math.abs(i - y), Math.abs(i - x) + 1)]++;
26
27 // Remove overcounted paths to boundaries (node 0 and node n-1)
28 // This accounts for paths that go through the shortcut unnecessarily
29 diff[Math.min(Math.abs(i - 0), Math.abs(i - y) + 1 + Math.abs(x - 0))]--;
30 diff[Math.min(Math.abs(i - (n - 1)), Math.abs(i - x) + 1 + Math.abs(y - (n - 1)))]--;
31
32 // Handle the middle section between x and y
33 // Remove contributions for distances within the shortcut region
34 const leftDistance = Math.max(x - i, 0); // Distance to x from left side
35 const rightDistance = Math.max(i - y, 0); // Distance to y from right side
36 const shortcutLength = y - x;
37
38 // Remove overcounted distances for the midpoint regions
39 diff[leftDistance + rightDistance + Math.floor(shortcutLength / 2)]--;
40 diff[leftDistance + rightDistance + Math.floor((shortcutLength + 1) / 2)]--;
41 }
42
43 // Convert difference array to actual counts using prefix sum
44 for (let i = 0; i + 1 < n; i++) {
45 diff[i + 1] += diff[i];
46 }
47
48 return diff;
49}
50
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of several main parts:
- Initial setup and list creation with
[2 * x for x in reversed(range(n2))]
takesO(n2)
time wheren2 = n - cycle_len + 2
, which isO(n)
in the worst case - The first loop for
res2
runs forcycle_len >> 1
iterations, which is at mostO(n)
- Processing two tails (
tail1
andtail2
), each involving:- Creating
res3
list with sizei_mx
which isO(tail + cycle_len/2)
=O(n)
- Multiple loops that iterate over the tail length or portions of
res3
, each taking at mostO(n)
time - The nested structure with
enumerate(range(4, val_mx, 4))
iteratesO(tail)
times
- Creating
- Final accumulation loops are all bounded by
O(n)
Since all operations are sequential and each is bounded by O(n)
, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space usage includes:
res
list of sizen
:O(n)
res2
list of sizecycle_len >> 1
:O(n)
in worst caseres3
list created for each tail with sizei_mx = tail + (cycle_len >> 1)
:O(n)
in worst case- Other variables use constant space:
O(1)
The lists are not all allocated simultaneously for their maximum sizes, but the dominant space usage is the res
array and temporary arrays that can be up to O(n)
in size. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling the Case When x > y
One of the most common mistakes is not properly normalizing the input when x > y
. The algorithm assumes x < y
for consistent processing of left and right tails.
Pitfall Example:
# Wrong: Using x and y directly without normalization left_tail = x - 1 right_tail = n - y
Solution:
Always ensure x < y
before processing:
if x > y: x, y = y, x left_tail = x - 1 right_tail = n - y
2. Off-by-One Errors in Cycle Length Calculation
The cycle length should include both endpoints plus the path between them.
Pitfall Example:
# Wrong: Forgetting to add 1
cycle_length = abs(x - y) # Missing the +1
Solution:
cycle_length = abs(x - y) + 1
3. Mishandling Even vs Odd Cycle Length
Even-length cycles have special properties where antipodal nodes have two equal-length shortest paths. This requires different handling for distance counting.
Pitfall Example:
# Wrong: Treating all cycles the same cycle_contribution = [cycle_length * 2] * (cycle_length // 2) # Missing the special case for even cycles
Solution:
cycle_contribution = [cycle_length * 2] * (cycle_length // 2) if cycle_length % 2 == 0: # Even cycle special case cycle_contribution[-1] = cycle_length # Antipodal pairs counted once
4. Incorrect Array Indexing When Building Tail Contributions
The symmetric pattern for tail contributions requires careful index calculation, especially for the secondary maximum index.
Pitfall Example:
# Wrong: Incorrect calculation of max_index_2 max_index_2 = max_index - 1 # Should consider cycle parity
Solution:
max_index_2 = max_index - (1 - (cycle_length % 2))
5. Forgetting to Pad the Result Array
The result array must have exactly n
elements, but the contracted graph might produce fewer distances.
Pitfall Example:
# Wrong: Returning array with incorrect length
result = [2 * distance for distance in reversed(range(contracted_nodes))]
return result # May have fewer than n elements
Solution:
result = [2 * distance for distance in reversed(range(contracted_nodes))]
while len(result) < n:
result.append(0)
6. Double-Counting Cycle Endpoints
When calculating cycle contributions, the entry and exit points of the cycle should not be double-counted with the contracted graph distances.
Pitfall Example:
# Wrong: Not adjusting for endpoints cycle_contribution[0] = cycle_length * 2 # Should subtract 2
Solution:
cycle_contribution[0] -= 2 # Avoid double-counting endpoints
7. Misunderstanding the Problem's Distance Definition
The problem asks for the number of pairs at each distance, not just counting unique distances. Each pair (i, j)
where i ≠ j
should be counted.
Pitfall Example:
# Wrong: Counting only one direction
return [x for x in reversed(range(n))] # Missing factor of 2
Solution:
return [2 * x for x in reversed(range(n))] # Count both (i,j) and (j,i)
Which of the following array represent a max heap?
Recommended Readings
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!