diff --git a/problems/3161-block-placement-queries/analysis_daily_20260530.md b/problems/3161-block-placement-queries/analysis_daily_20260530.md new file mode 100644 index 0000000..3d5463b --- /dev/null +++ b/problems/3161-block-placement-queries/analysis_daily_20260530.md @@ -0,0 +1,82 @@ +# 3161. Block Placement Queries + +[LeetCode Link](https://leetcode.com/problems/block-placement-queries/) + +Difficulty: Hard +Topics: Array, Binary Search, Binary Indexed Tree, Segment Tree +Acceptance Rate: 24.4% + +## Hints + +### Hint 1 + +For any query `[2, x, sz]`, the only thing that matters about the obstacles is the set of *gaps* between consecutive obstacles inside `[0, x]`, plus the leftover space between the largest obstacle ≤ `x` and `x` itself. A block of size `sz` fits if and only if one of those gaps is at least `sz`. So the real question is: how do you maintain "the maximum gap between consecutive obstacles" while obstacles are being inserted one at a time? + +### Hint 2 + +Inserting an obstacle changes at most two gaps: the gap landing at the new obstacle, and the gap landing at its successor (the previous "long" gap gets split into two). That locality is what makes a segment tree feasible. Coordinate-compress every position you will ever see (all `x` values from both query types, plus a sentinel at 0), then index a segment tree by compressed position. Pair it with a way to find the predecessor and successor of any position among the currently-placed obstacles. + +### Hint 3 + +Use **two** segment trees over the compressed positions: + +1. A *presence* tree (sum / "exists" semantics) that lets you descend to find the rightmost obstacle ≤ index `i` (predecessor) and leftmost obstacle ≥ index `i` (successor). +2. A *gap* tree (range-max) where `gap[i]` stores the distance from the obstacle at compressed index `i` to its predecessor obstacle. Inserting `x` updates `gap[x] = x - pred` and `gap[succ] = succ - x`. + +Treat position `0` as a permanent sentinel obstacle so the very first real gap (from origin to the first obstacle) is handled by the same formula. A query `[2, x, sz]` then computes `max(rangeMax(gap, 1..idx(pred)), x - pred) ≥ sz`, where `pred` is the largest obstacle position ≤ `x`. + +## Approach + +**Setup.** Collect every distinct position you will ever touch — all `x` values from queries of both types and the sentinel `0` — then sort and assign each a compressed index. Build two segment trees over those indices: + +- `presence[i]` ∈ {0, 1} tells whether an obstacle is currently at the position with compressed index `i`. Internal nodes hold the sum, but more importantly we use the tree to *descend* to the rightmost / leftmost `1` in a range. +- `gap[i]` stores, for each currently active obstacle, the distance to its immediate predecessor obstacle (with `0` acting as the predecessor of the leftmost real obstacle). Internal nodes hold the max. + +Mark the sentinel: `presence[idx(0)] = 1`, `gap[idx(0)] = 0`. + +**Type 1 — insert obstacle at `x`.** Let `xi = idx(x)`. + +1. `predIdx = rightmost 1 in presence[0..xi-1]` — guaranteed to exist thanks to the sentinel. Let `predPos = positions[predIdx]`. +2. `succIdx = leftmost 1 in presence[xi+1..n-1]`, possibly `-1` if none. +3. Set `presence[xi] = 1`. +4. Set `gap[xi] = x - predPos` (the new gap ending at `x`). +5. If `succIdx ≠ -1`, set `gap[succIdx] = positions[succIdx] - x` (the old gap, which used to span from `predPos` to `positions[succIdx]`, is now shortened on the left). + +Each insert touches O(log n) nodes in each tree. + +**Type 2 — query `(x, sz)`.** Let `xi = idx(x)`. + +1. `predIdx = rightmost 1 in presence[0..xi]` (note: includes `xi` itself in case `x` *is* an obstacle). +2. `predPos = positions[predIdx]`. +3. `interiorMax = rangeMax(gap, 1..predIdx)` — we skip index 0 (the sentinel's gap is 0 by convention). +4. `trailing = x - predPos` — the leftover space from the last obstacle ≤ `x` up to `x`. +5. Answer is `max(interiorMax, trailing) ≥ sz`. + +The `trailing` term matters because the segment tree only stores gaps that *end at an obstacle*; the gap from the last obstacle to `x` has no obstacle on its right and isn't stored anywhere, so we add it explicitly. + +**Worked example (Example 2).** `queries = [[1,7],[2,7,6],[1,2],[2,7,5],[2,7,6]]`. Compressed positions are `[0, 2, 7]`. + +- `[1,7]` → predecessor of 7 is 0; insert. `gap[idx(7)] = 7`. +- `[2,7,6]` → pred = 7, interior max over `gap` indices `[1, idx(7)]` = 7, trailing = 0. `7 ≥ 6` → **true**. +- `[1,2]` → predecessor of 2 is 0, successor is 7. Insert. `gap[idx(2)] = 2`, `gap[idx(7)] = 7 - 2 = 5`. +- `[2,7,5]` → pred = 7, interior max = `max(2, 5) = 5`, trailing = 0. `5 ≥ 5` → **true**. +- `[2,7,6]` → same, `5 ≥ 6` → **false**. + +Result: `[true, true, false]`. ✓ + +This is genuinely a hard problem — the trick of *coordinate-compressing query positions too* (not just obstacle positions) is what makes the predecessor query fast, and the locality observation about gaps is what keeps inserts cheap. If you got stuck thinking the obstacles needed to be maintained in a balanced BST or skip list, that intuition is right; coordinate compression + a segment tree just gives you the same power without writing a BST in Go. + +## Complexity Analysis + +Time Complexity: O((Q + P) log P), where `Q` is the number of queries and `P` is the number of distinct positions across all queries (`P ≤ Q + 1`). Each query performs a constant number of segment-tree operations, each O(log P). + +Space Complexity: O(P) for the compressed-position table and the two segment trees. + +## Edge Cases + +- **No obstacle ≤ `x`.** The sentinel at 0 still acts as the predecessor, so `predPos = 0`, `trailing = x`, and the answer reduces to `x ≥ sz` — exactly the empty-line case. +- **`x` itself is an obstacle.** Predecessor lookup includes `xi`, so `predPos = x` and `trailing = 0`. The block has to live entirely in one of the interior gaps; we don't double-count anything past `x`. +- **First obstacle inserted.** No successor exists, so step 5 of insert is skipped. The first real gap (origin to obstacle) is captured cleanly because `predPos` is the sentinel `0`. +- **Inserting between two existing obstacles.** Both `gap[xi]` and `gap[succIdx]` are rewritten in the same call; the old combined gap effectively disappears from the max tree (replaced by the smaller of its two halves). +- **Query for a tiny `sz`.** Even `sz = 1` always passes whenever `x ≥ 1` and at least one gap (including trailing) is ≥ 1 — typically trivially true. +- **Query `x` equals a position never used by a type-1 query.** Coordinate compression must include all `x` values from type-2 queries too, otherwise `idx(x)` would be undefined. This is why we collect positions from both query types. diff --git a/problems/3161-block-placement-queries/problem.md b/problems/3161-block-placement-queries/problem.md new file mode 100644 index 0000000..2d22632 --- /dev/null +++ b/problems/3161-block-placement-queries/problem.md @@ -0,0 +1,71 @@ +--- +number: "3161" +frontend_id: "3161" +title: "Block Placement Queries" +slug: "block-placement-queries" +difficulty: "Hard" +topics: + - "Array" + - "Binary Search" + - "Binary Indexed Tree" + - "Segment Tree" +acceptance_rate: 2439.9 +is_premium: false +created_at: "2026-05-30T04:32:14.429374+00:00" +fetched_at: "2026-05-30T04:32:14.429374+00:00" +link: "https://leetcode.com/problems/block-placement-queries/" +date: "2026-05-30" +--- + +# 3161. Block Placement Queries + +There exists an infinite number line, with its origin at 0 and extending towards the **positive** x-axis. + +You are given a 2D array `queries`, which contains two types of queries: + + 1. For a query of type 1, `queries[i] = [1, x]`. Build an obstacle at distance `x` from the origin. It is guaranteed that there is **no** obstacle at distance `x` when the query is asked. + 2. For a query of type 2, `queries[i] = [2, x, sz]`. Check if it is possible to place a block of size `sz` _anywhere_ in the range `[0, x]` on the line, such that the block **entirely** lies in the range `[0, x]`. A block **cannot** be placed if it intersects with any obstacle, but it may touch it. Note that you do**not** actually place the block. Queries are separate. + + + +Return a boolean array `results`, where `results[i]` is `true` if you can place the block specified in the `ith` query of type 2, and `false` otherwise. + + + +**Example 1:** + +**Input:** queries = [[1,2],[2,3,3],[2,3,1],[2,2,2]] + +**Output:** [false,true,true] + +**Explanation:** + +**![](https://assets.leetcode.com/uploads/2024/04/22/example0block.png)** + +For query 0, place an obstacle at `x = 2`. A block of size at most 2 can be placed before `x = 3`. + +**Example 2:** + +**Input:** queries = [[1,7],[2,7,6],[1,2],[2,7,5],[2,7,6]] + +**Output:** [true,true,false] + +**Explanation:** + +**![](https://assets.leetcode.com/uploads/2024/04/22/example1block.png)** + + * Place an obstacle at `x = 7` for query 0. A block of size at most 7 can be placed before `x = 7`. + * Place an obstacle at `x = 2` for query 2. Now, a block of size at most 5 can be placed before `x = 7`, and a block of size at most 2 before `x = 2`. + + + + + +**Constraints:** + + * `1 <= queries.length <= 15 * 104` + * `2 <= queries[i].length <= 3` + * `1 <= queries[i][0] <= 2` + * `1 <= x, sz <= min(5 * 104, 3 * queries.length)` + * The input is generated such that for queries of type 1, no obstacle exists at distance `x` when the query is asked. + * The input is generated such that there is at least one query of type 2. diff --git a/problems/3161-block-placement-queries/solution_daily_20260530.go b/problems/3161-block-placement-queries/solution_daily_20260530.go new file mode 100644 index 0000000..eadc6cd --- /dev/null +++ b/problems/3161-block-placement-queries/solution_daily_20260530.go @@ -0,0 +1,194 @@ +package main + +// Approach: coordinate-compress every position that appears in any query +// (plus a sentinel at 0), then maintain two segment trees indexed by +// compressed position: +// +// presence[i] = 1 iff an obstacle currently sits at positions[i] +// (sum-aggregated; used to descend to predecessor / successor) +// gap[i] = positions[i] - positions[prevObstacle(i)] for each +// active obstacle (max-aggregated) +// +// Inserting x updates gap at x and at x's successor — exactly the two +// gaps that change. A type-2 query (x, sz) takes max(rangeMax over gap +// for indices in (0, idx(pred)]) and (x - pred), and checks against sz. + +import "sort" + +type segPresence struct { + size int + tree []int +} + +func newSegPresence(n int) *segPresence { + size := 1 + for size < n { + size <<= 1 + } + return &segPresence{size: size, tree: make([]int, 2*size)} +} + +func (s *segPresence) update(i, v int) { + i += s.size + s.tree[i] = v + for i > 1 { + i >>= 1 + s.tree[i] = s.tree[2*i] + s.tree[2*i+1] + } +} + +// findPredecessor returns the largest index in [0, lim] whose value is 1, +// or -1 if none exists. +func (s *segPresence) findPredecessor(lim int) int { + if lim < 0 { + return -1 + } + return s.findPredRec(1, 0, s.size-1, lim) +} + +func (s *segPresence) findPredRec(node, l, r, lim int) int { + if l > lim || s.tree[node] == 0 { + return -1 + } + if l == r { + return l + } + mid := (l + r) >> 1 + if right := s.findPredRec(2*node+1, mid+1, r, lim); right != -1 { + return right + } + return s.findPredRec(2*node, l, mid, lim) +} + +// findSuccessor returns the smallest index in [lim, n-1] whose value is 1, +// or -1 if none exists. +func (s *segPresence) findSuccessor(lim int) int { + if lim >= s.size { + return -1 + } + return s.findSuccRec(1, 0, s.size-1, lim) +} + +func (s *segPresence) findSuccRec(node, l, r, lim int) int { + if r < lim || s.tree[node] == 0 { + return -1 + } + if l == r { + return l + } + mid := (l + r) >> 1 + if left := s.findSuccRec(2*node, l, mid, lim); left != -1 { + return left + } + return s.findSuccRec(2*node+1, mid+1, r, lim) +} + +type segMax struct { + size int + tree []int +} + +func newSegMax(n int) *segMax { + size := 1 + for size < n { + size <<= 1 + } + return &segMax{size: size, tree: make([]int, 2*size)} +} + +func (s *segMax) update(i, v int) { + i += s.size + s.tree[i] = v + for i > 1 { + i >>= 1 + left, right := s.tree[2*i], s.tree[2*i+1] + if left >= right { + s.tree[i] = left + } else { + s.tree[i] = right + } + } +} + +func (s *segMax) queryMax(l, r int) int { + if l > r { + return 0 + } + res := 0 + l += s.size + r += s.size + 1 + for l < r { + if l&1 == 1 { + if s.tree[l] > res { + res = s.tree[l] + } + l++ + } + if r&1 == 1 { + r-- + if s.tree[r] > res { + res = s.tree[r] + } + } + l >>= 1 + r >>= 1 + } + return res +} + +func getResults(queries [][]int) []bool { + // Collect every distinct position we might need to address, plus the + // sentinel at 0. We need x values from type-2 queries too, because + // findPredecessor / queryMax operate on compressed indices. + posSet := map[int]struct{}{0: {}} + for _, q := range queries { + posSet[q[1]] = struct{}{} + } + positions := make([]int, 0, len(posSet)) + for p := range posSet { + positions = append(positions, p) + } + sort.Ints(positions) + + posIdx := make(map[int]int, len(positions)) + for i, p := range positions { + posIdx[p] = i + } + n := len(positions) + + pres := newSegPresence(n) + gaps := newSegMax(n) + + // Mark the sentinel at position 0 as an obstacle (its gap stays 0). + pres.update(posIdx[0], 1) + + results := make([]bool, 0, len(queries)) + for _, q := range queries { + switch q[0] { + case 1: + x := q[1] + xi := posIdx[x] + predIdx := pres.findPredecessor(xi - 1) + predPos := positions[predIdx] + succIdx := pres.findSuccessor(xi + 1) + pres.update(xi, 1) + gaps.update(xi, x-predPos) + if succIdx != -1 { + gaps.update(succIdx, positions[succIdx]-x) + } + case 2: + x, sz := q[1], q[2] + xi := posIdx[x] + predIdx := pres.findPredecessor(xi) + predPos := positions[predIdx] + interior := gaps.queryMax(1, predIdx) + trailing := x - predPos + best := interior + if trailing > best { + best = trailing + } + results = append(results, best >= sz) + } + } + return results +} diff --git a/problems/3161-block-placement-queries/solution_daily_20260530_test.go b/problems/3161-block-placement-queries/solution_daily_20260530_test.go new file mode 100644 index 0000000..924f09e --- /dev/null +++ b/problems/3161-block-placement-queries/solution_daily_20260530_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestGetResultsDaily20260530(t *testing.T) { + tests := []struct { + name string + queries [][]int + expected []bool + }{ + { + name: "example 1: obstacle at 2 limits the largest fitting block", + queries: [][]int{{1, 2}, {2, 3, 3}, {2, 3, 1}, {2, 2, 2}}, + expected: []bool{false, true, true}, + }, + { + name: "example 2: inserting a second obstacle splits the gap", + queries: [][]int{{1, 7}, {2, 7, 6}, {1, 2}, {2, 7, 5}, {2, 7, 6}}, + expected: []bool{true, true, false}, + }, + { + name: "edge case: no obstacles, block fits exactly in [0, x]", + queries: [][]int{{2, 5, 5}}, + expected: []bool{true}, + }, + { + name: "edge case: no obstacles, block strictly larger than x", + queries: [][]int{{2, 5, 6}}, + expected: []bool{false}, + }, + { + name: "edge case: query x equals an obstacle position (touching allowed)", + queries: [][]int{{1, 3}, {1, 5}, {2, 3, 3}}, + expected: []bool{true}, + }, + { + name: "edge case: obstacle sits beyond the query range", + queries: [][]int{{1, 10}, {2, 5, 5}}, + expected: []bool{true}, + }, + { + name: "edge case: two obstacles create small gaps, trailing decides feasibility", + queries: [][]int{{1, 1}, {1, 3}, {2, 5, 2}, {2, 5, 3}}, + expected: []bool{true, false}, + }, + { + name: "edge case: obstacle at 1, query for size that only fits trailing", + queries: [][]int{{1, 1}, {2, 5, 4}, {2, 5, 5}}, + expected: []bool{true, false}, + }, + { + name: "edge case: dense obstacles still leave a touch-fit block of size 1", + queries: [][]int{{1, 1}, {1, 2}, {1, 3}, {2, 3, 1}, {2, 3, 2}}, + expected: []bool{true, false}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getResults(tt.queries) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("getResults(%v) = %v, want %v", tt.queries, got, tt.expected) + } + }) + } +}