
Community: Array and Hashing
Array and Hashing intro
OA questions found on threads and comment sections. Open source!
What is a Array
To be or not to be
Building Density House Building Query - Medium
Intro
You are monitoring the building density in a district of houses. The district is represented as a number line, where a house can be built at each numbered point on the line if at least one of the neighboring points is not occupied. Initially, there are no houses in the district. You are given queries, an array of integers representing the locations of new houses in the order in which they will be built. After each house is built, your task is to find the longest segment of contiguous houses in the district. Return an array of integers representing the longest segment of contiguous houses after each respective house from queries is built. NOTE: It's guaranteed that all of the house locations in queries are unique and no house was built at a point with existing houses on both left and right adjacent points. Assume that array indices are 0-based.
| Example Input | Output |
|---|---|
| queries = [2, 1, 3] | [1, 2, 3] |
| queries = [1, 3, 0, 4] | [1, 1, 2, 2] |
Abstraction
Given a stream of house-building queries, return the longest contiguous segment of houses after each individual build.
Streaming Version Of A Batch Problem: This is the incremental/online version of finding the longest run of consecutive integers (same subject matter as LC 128 - Longest Consecutive Sequence, which solves it as a single batch over a static, complete array). Here, houses arrive one at a time and an answer is required after every single insertion, not just once at the end.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Hash Map] Boundary Length Tracking With Merge On Insert - Hash Map/Segment Endpoint Merging
def solution(self, queries: List[int]) -> List[int]:
# Boundary Length Tracking:
# Instead of storing every house individually and re-scanning the
# district after each insert, track segment length only at the two
# ENDPOINTS of each contiguous segment (leftmost and rightmost house
# in that segment). When a new house is built, it can only ever
# merge with the segment ending immediately to its left and/or the
# segment starting immediately to its right -- so a single O(1)
# dictionary lookup on each side is enough to compute the new
# segment's length, without touching any of the houses in between.
# Why This Works:
# A newly built house x can only affect its immediate neighbors
# (x-1 and x+1), since houses further away are already part of
# separate, non-adjacent segments unaffected by this insert. If x-1
# is occupied, it's the right end of some existing segment; if x+1
# is occupied, it's the left end of some existing segment. Merging
# is just: new length = left segment length + right segment length + 1.
# sc: O(n), one entry per stored segment endpoint, n = len(queries)
length = {}
# sc: O(n), holds one result per query
res = []
# tracks the longest segment seen so far across all queries
# sc: O(1)
maxLen = 0
# tc: O(n)
for x in queries:
# Look Left:
# length of segment ending at x-1, 0 if x-1 is unoccupied
leftLen = length.get(x - 1, 0)
# Look Right:
# length of segment starting at x+1, 0 if x+1 is unoccupied
rightLen = length.get(x + 1, 0)
# Merge:
# new segment spans however far left and right it now reaches,
# plus x itself
newLen = leftLen + rightLen + 1
# Update Boundaries:
# only the two new outermost endpoints of the merged segment
# need updating -- old inner boundary entries (at x-1 and x+1,
# if they existed) are now stale and safe to leave in place,
# since a future lookup will always resolve to the correct
# outer endpoint of whatever segment it belongs to
length[x - leftLen] = newLen
length[x + rightLen] = newLen
# Track running maximum across all queries so far
maxLen = max(maxLen, newLen)
res.append(maxLen)
# overall: tc O(n), one O(1) amortized dict lookup/update per query
# overall: sc O(n), length dict and res both scale with number of queries
return resSegment Count After House Destruction
Intro
Given an array of houses like houses = [1,2,3,7,8,10,11] and an array of queries like q=[2,10,8], return an array of how many segments exist after each query. Each query indicates the house that will be destroyed and the queries are executed in order. A segment refers to a consecutive group of houses. There can technically be one house in a segment if there are no other houses that are consecutive to it (it doesn't have neighbors), however it is still one segment.
| Example Input | Output |
|---|---|
| houses = [1,2,3,7,8,10,11], queries = [2,10,8] | [4, 4, 4] |
Abstraction
Given a static set of houses and a stream of destruction queries, return the total number of segments remaining after each individual destruction.
Mirror Image Of Building Density: This is the inverse of the house-building problem above -- houses are removed instead of added, so segments can only split or shrink, never merge or grow. The same core principle still applies: a single removal only ever has local effects on its immediate neighbors, so an O(1)-per-query incremental update is possible instead of a full rescan.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Hash Set] Neighbor Check Before Removal - Hash Set/Incremental Segment Count Maintenance
def solution(self, houses: List[int], queries: List[int]) -> List[int]:
# Neighbor Check Before Removal:
# Rather than rescanning the whole house line after each destruction
# to recount segments, maintain a running segment count and update it
# incrementally. Destroying house x can only ever affect x's own
# immediate neighbors (x-1, x+1) -- houses farther away are already
# part of separate, non-adjacent segments untouched by this removal.
# The Three Cases When Destroying House x:
# 1. Neither neighbor exists (x was an isolated, single-house
# segment) -> removing x eliminates that whole segment
# -> segment count DECREASES by 1
# 2. Exactly one neighbor exists (x was at the END of a segment)
# -> the segment just shrinks by one house, still one segment
# -> segment count STAYS THE SAME
# 3. Both neighbors exist (x was in the MIDDLE of a segment)
# -> removing x splits one segment into two
# -> segment count INCREASES by 1
# Why check neighbors BEFORE removing x:
# x-1/x+1 membership must be evaluated against the state of the
# house set as it exists right before x is destroyed -- this is
# what determines which of the 3 cases applies for this query.
# sc: O(h), stores every currently-standing house, h = len(houses)
standing = set(houses)
# initial segment count = number of maximal runs in 'houses' before
# any queries are applied
# tc: O(h log h) to sort, O(h) to scan for run boundaries
houses_sorted = sorted(houses)
segCount = 0
# tc: O(h)
for i, x in enumerate(houses_sorted):
# a new segment starts wherever the previous house isn't x-1,
# i.e. there's a gap (or this is the very first house)
if i == 0 or houses_sorted[i] != houses_sorted[i - 1] + 1:
segCount += 1
# sc: O(q), holds one result per query, q = len(queries)
res = []
# tc: O(q)
for x in queries:
# Look Left / Look Right:
# check standing-neighbor status BEFORE removing x
hasLeft = (x - 1) in standing
hasRight = (x + 1) in standing
# Apply the 3 cases:
if not hasLeft and not hasRight:
# Case 1: isolated house, its segment disappears entirely
segCount -= 1
elif hasLeft and hasRight:
# Case 3: house was in the middle, splits one segment into two
segCount += 1
# Case 2 (exactly one neighbor): segCount unchanged
# Destroy the house
standing.remove(x)
res.append(segCount)
# overall: tc O(h log h + q), dominated by the one-time initial sort;
# each query itself is O(1) amortized (2 set lookups + 1
# removal), so q queries cost O(q) total
# overall: sc O(h + q), standing set scales with houses, res with queries
return res