diff --git a/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/analysis.md b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/analysis.md new file mode 100644 index 0000000..fdde1d2 --- /dev/null +++ b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/analysis.md @@ -0,0 +1,146 @@ +# 1621. Number of Sets of K Non-Overlapping Line Segments + +[LeetCode Link](https://leetcode.com/problems/number-of-sets-of-k-non-overlapping-line-segments/) + +Difficulty: Medium +Topics: Math, Dynamic Programming, Combinatorics, Prefix Sum +Acceptance Rate: 58.2% + +## Hints + +### Hint 1 + +Nothing about the points matters except their order — point `i` sits at `x = i`, so a +segment is fully described by the pair of indices `(a, b)` with `a < b`. A set of `k` +non-overlapping segments is therefore just a sequence of index pairs + +``` +a1 < b1 <= a2 < b2 <= a3 < b3 <= ... <= ak < bk +``` + +(sorted left to right; `b_i <= a_{i+1}` because segments may touch at an endpoint but +may not overlap). So the question is: *how many such sequences of `2k` indices are +there?* That reframing alone is most of the work. Think "count monotone sequences," +either with dynamic programming or with a counting argument. + +### Hint 2 + +The straightforward route is DP. Scan the points left to right and track two things: +how many segments you have completed, and whether the segment you are currently +building is still "open." That gives states like `dp[i][j][0/1]` = ways to process the +first `i` points having finished `j` segments, with the last one closed (`0`) or still +open (`1`). Every transition is O(1), so the whole thing is O(n·k) — fast enough for +`n <= 1000`. Write that down and check it against `n = 4, k = 2 -> 5`. + +But look at the chain of inequalities again. It is *almost* strictly increasing: the +only non-strict steps are the `b_i <= a_{i+1}` ones, and there are exactly `k - 1` of +them. Can you make those steps strict? + +### Hint 3 + +Yes — shift each segment right by how many segments precede it. Map the `i`-th +segment `(a_i, b_i)` (1-indexed) to `(a_i + i - 1, b_i + i - 1)`. Each `<=` in the +chain becomes `<`, so the `2k` shifted values are **strictly increasing**. They live in +`[0, (n - 1) + (k - 1)] = [0, n + k - 2]`, a range with `n + k - 1` distinct values. + +The map is reversible: given any strictly increasing `2k` values in that range, +subtract `i - 1` from the `i`-th pair to recover a valid configuration. It is a +bijection, so the answer is a single binomial coefficient: + +``` +answer = C(n + k - 1, 2k) (mod 1e9 + 7) +``` + +## Approach + +The solution used here is the closed form. The reasoning, in full: + +**Step 1 — normalize the configuration.** A set of `k` non-overlapping segments has a +canonical left-to-right ordering. Writing the `i`-th segment as `(a_i, b_i)`, the +constraints "each segment covers two or more points" and "segments may share endpoints +but not overlap" become exactly + +``` +0 <= a1 < b1 <= a2 < b2 <= ... <= ak < bk <= n - 1 +``` + +Sets of segments correspond one-to-one with such chains, so counting chains counts +sets. + +**Step 2 — make the chain strictly increasing.** The chain alternates between strict +`<` (inside a segment) and non-strict `<=` (between segments). There are `k - 1` +non-strict steps. Define + +``` +a_i' = a_i + (i - 1) b_i' = b_i + (i - 1) +``` + +Adding a larger offset to each successive segment breaks every tie: if `b_i = a_{i+1}` +then `b_i' = b_i + i - 1 < a_{i+1} + i = a_{i+1}'`. Now + +``` +0 <= a1' < b1' < a2' < b2' < ... < ak' < bk' <= (n - 1) + (k - 1) = n + k - 2 +``` + +**Step 3 — count.** The primed values are `2k` *distinct* numbers chosen from the +`n + k - 1` values `{0, 1, ..., n + k - 2}`, and since they are sorted, the choice of +the set determines the sequence. Conversely, any `2k`-element subset, sorted and +un-shifted, yields a valid configuration (subtracting the offsets preserves the +required inequalities). The correspondence is a bijection, hence + +``` +answer = C(n + k - 1, 2k) +``` + +**Step 4 — compute it mod 1e9 + 7.** Build the binomial multiplicatively: + +``` +C(N, r) = product over i in [0, r) of (N - i) / (i + 1) +``` + +Accumulate the numerator and denominator separately modulo the prime `p = 1e9 + 7`, +then divide once at the end using Fermat's little theorem: `den^(p-2) ≡ den^(-1) +(mod p)`. Dividing only once keeps the code short and avoids any need for a full +factorial table. Note that `n + k - 1 <= 1998 < p`, so no factor in the denominator is +ever a multiple of `p` and the inverse always exists. + +**Worked example (`n = 4, k = 2`).** The five configurations are `{(0,2),(2,3)}`, +`{(0,1),(1,3)}`, `{(0,1),(2,3)}`, `{(1,2),(2,3)}`, `{(0,1),(1,2)}`. Shifting the second +segment right by one turns them into the strictly increasing quadruples +`0<2<3<4`, `0<1<2<4`, `0<1<3<4`, `1<2<3<4`, `0<1<2<3` — exactly the +`C(5, 4) = 5` four-element subsets of `{0,1,2,3,4}`. Likewise `n = 3, k = 1` gives +`C(3, 2) = 3`, and `n = 30, k = 7` gives `C(36, 14) = 3796297200 ≡ 796297179`. + +If the bijection does not feel convincing yet, the O(n·k) DP from Hint 2 is a perfectly +good accepted solution and is worth writing first — then compare its table against the +binomials and watch Pascal's rule fall out. Be honest with yourself: the shift trick is +the kind of insight that is obvious *afterwards*, and most people reach it by first +staring at a DP table. + +## Complexity Analysis + +Time Complexity: O(k + log MOD) — one pass of `2k <= 2(n-1)` multiplications to build +the binomial, plus a single modular exponentiation for the inverse. This is +O(n) overall, versus O(n·k) for the DP formulation. + +Space Complexity: O(1) — only a handful of accumulators; no DP table is materialized. + +## Edge Cases + +- **`k = 1`**: reduces to `C(n, 2)`, the number of index pairs. A good sanity check + that the formula is not off by one. +- **`k = n - 1` (the maximum allowed)**: `C(n + k - 1, 2k) = C(2n - 2, 2n - 2) = 1` — + the only option is to chain every unit segment `(0,1), (1,2), ..., (n-2,n-1)`. Any + off-by-one in the shift would break this case loudly. +- **Smallest input `n = 2, k = 1`**: `C(2, 2) = 1`. Guards against loops that assume + at least two segments or at least three points. +- **`2k > n + k - 1`**: mathematically impossible under the stated constraints + (`k <= n - 1`), but the binomial helper still returns `0` for out-of-range `r` rather + than producing garbage, so the code is safe if the constraints are ever relaxed. +- **Overflow**: every intermediate product is of two values below `1e9 + 7`, which fits + comfortably in a 64-bit Go `int`. Reducing after each multiplication is what keeps it + that way — dropping a single `% MOD` silently corrupts large inputs like + `n = 1000, k = 500`. +- **Modulus handling**: the answer must be taken mod `1e9 + 7` even when the true count + is small; returning the raw product is wrong only for large inputs, which is exactly + the kind of bug that passes the sample tests and fails on submission. diff --git a/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/problem.md b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/problem.md new file mode 100644 index 0000000..4ae5fec --- /dev/null +++ b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/problem.md @@ -0,0 +1,60 @@ +--- +number: "1621" +frontend_id: "1621" +title: "Number of Sets of K Non-Overlapping Line Segments" +slug: "number-of-sets-of-k-non-overlapping-line-segments" +difficulty: "Medium" +topics: + - "Math" + - "Dynamic Programming" + - "Combinatorics" + - "Prefix Sum" +acceptance_rate: 5820.5 +is_premium: false +created_at: "2026-09-16T05:02:27.030395+00:00" +fetched_at: "2026-09-16T05:02:27.030395+00:00" +link: "https://leetcode.com/problems/number-of-sets-of-k-non-overlapping-line-segments/" +date: "2026-09-16" +--- + +# 1621. Number of Sets of K Non-Overlapping Line Segments + +Given `n` points on a 1-D plane, where the `ith` point (from `0` to `n-1`) is at `x = i`, find the number of ways we can draw **exactly** `k` **non-overlapping** line segments such that each segment covers two or more points. The endpoints of each segment must have **integral coordinates**. The `k` line segments **do not** have to cover all `n` points, and they are **allowed** to share endpoints. + +Return _the number of ways we can draw_`k` _non-overlapping line segments_ _._ Since this number can be huge, return it **modulo** `109 + 7`. + + + +**Example 1:** + +![](https://assets.leetcode.com/uploads/2020/09/07/ex1.png) + + + **Input:** n = 4, k = 2 + **Output:** 5 + **Explanation:** The two line segments are shown in red and blue. + The image above shows the 5 different ways {(0,2),(2,3)}, {(0,1),(1,3)}, {(0,1),(2,3)}, {(1,2),(2,3)}, {(0,1),(1,2)}. + + +**Example 2:** + + + **Input:** n = 3, k = 1 + **Output:** 3 + **Explanation:** The 3 ways are {(0,1)}, {(0,2)}, {(1,2)}. + + +**Example 3:** + + + **Input:** n = 30, k = 7 + **Output:** 796297179 + **Explanation:** The total number of possible ways to draw 7 line segments is 3796297200. Taking this number modulo 109 + 7 gives us 796297179. + + + + +**Constraints:** + + * `2 <= n <= 1000` + * `1 <= k <= n-1` diff --git a/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916.go b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916.go new file mode 100644 index 0000000..639f762 --- /dev/null +++ b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916.go @@ -0,0 +1,54 @@ +package main + +// 1621. Number of Sets of K Non-Overlapping Line Segments +// +// A set of k non-overlapping segments is a chain of indices +// +// 0 <= a1 < b1 <= a2 < b2 <= ... <= ak < bk <= n-1 +// +// Shifting the i-th segment (1-indexed) right by i-1 turns every "<=" into "<", +// so the 2k shifted values are strictly increasing and live in [0, n+k-2], a +// range of n+k-1 values. The shift is reversible, so configurations correspond +// one-to-one with 2k-element subsets of that range: +// +// answer = C(n+k-1, 2k) mod 1e9+7 +// +// The binomial is built multiplicatively, with a single modular inverse (via +// Fermat's little theorem) applied at the end. O(k + log MOD) time, O(1) space. + +const mod1621 = 1_000_000_007 + +func numberOfSets(n int, k int) int { + return binomMod1621(n+k-1, 2*k) +} + +// binomMod1621 returns C(n, r) modulo mod1621, and 0 when r is out of range. +func binomMod1621(n, r int) int { + if r < 0 || r > n { + return 0 + } + if r > n-r { + r = n - r + } + + num, den := 1, 1 + for i := 0; i < r; i++ { + num = num * ((n - i) % mod1621) % mod1621 + den = den * (i + 1) % mod1621 + } + return num * powMod1621(den, mod1621-2) % mod1621 +} + +// powMod1621 computes base^exp modulo mod1621 by fast exponentiation. +func powMod1621(base, exp int) int { + base %= mod1621 + result := 1 + for exp > 0 { + if exp&1 == 1 { + result = result * base % mod1621 + } + base = base * base % mod1621 + exp >>= 1 + } + return result +} diff --git a/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916_test.go b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916_test.go new file mode 100644 index 0000000..f9124ee --- /dev/null +++ b/problems/1621-number-of-sets-of-k-non-overlapping-line-segments/solution_daily_20260916_test.go @@ -0,0 +1,62 @@ +package main + +import "testing" + +func TestNumberOfSets(t *testing.T) { + tests := []struct { + name string + n int + k int + expected int + }{ + {"example 1: n=4, k=2 gives 5 arrangements", 4, 2, 5}, + {"example 2: n=3, k=1 counts every index pair", 3, 1, 3}, + {"example 3: n=30, k=7 needs the modulus", 30, 7, 796297179}, + {"edge case: smallest input n=2, k=1", 2, 1, 1}, + {"edge case: k=n-1 forces the unit-segment chain", 6, 5, 1}, + {"edge case: k=n-1 at the upper bound", 1000, 999, 1}, + {"edge case: k=1 reduces to C(n,2)", 1000, 1, 499500}, + {"edge case: n=5, k=2", 5, 2, 15}, + {"edge case: large n and k exercise modular reduction", 1000, 500, 70047606}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if result := numberOfSets(tt.n, tt.k); result != tt.expected { + t.Errorf("numberOfSets(%d, %d) = %v, want %v", tt.n, tt.k, result, tt.expected) + } + }) + } +} + +// TestNumberOfSetsAgainstBruteForce cross-checks the closed form against an +// explicit enumeration of every valid chain of segments for small inputs. +func TestNumberOfSetsAgainstBruteForce(t *testing.T) { + for n := 2; n <= 9; n++ { + for k := 1; k <= n-1; k++ { + want := bruteForceSets1621(n, k) + if got := numberOfSets(n, k); got != want { + t.Errorf("numberOfSets(%d, %d) = %v, want %v (brute force)", n, k, got, want) + } + } + } +} + +// bruteForceSets1621 enumerates chains a1 < b1 <= a2 < b2 <= ... < bk <= n-1, +// counting each set of segments exactly once via its left-to-right ordering. +func bruteForceSets1621(n, k int) int { + var count func(start, remaining int) int + count = func(start, remaining int) int { + if remaining == 0 { + return 1 + } + total := 0 + for a := start; a < n; a++ { + for b := a + 1; b < n; b++ { + total += count(b, remaining-1) + } + } + return total + } + return count(0, k) +}