Jc-alt logo
jc
Community: Array and Hashing

Community: Array and Hashing

··
8 min read
·data structures and algorithms

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 InputOutput
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 res

Segment 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 InputOutput
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