1621. Number of Sets of K Non-Overlapping Line Segments
Problem Description
You have n
points on a one-dimensional line, positioned at coordinates x = 0, 1, 2, ..., n-1
. Your task is to draw exactly k
non-overlapping line segments where:
-
Each segment must cover at least two points (a segment from point
i
to pointj
covers all points between and includingi
andj
) -
Segments cannot overlap - no two segments can share any interior points, though they can share endpoints
-
All endpoints must be at integral coordinates (at the given points)
-
You must draw exactly
k
segments - not more, not less -
Not all points need to be covered - some points can remain uncovered by any segment
The problem asks you to count the total number of different ways to draw these k
segments following all the rules above. Since the answer can be very large, return it modulo 10^9 + 7
.
For example, if n = 4
(points at positions 0, 1, 2, 3) and k = 2
, one valid way would be to draw segments [0,1]
and [2,3]
. Another valid way would be [0,2]
and [2,3]
(sharing endpoint at position 2).
The solution uses dynamic programming with two arrays:
f[i][j]
: number of ways to placej
segments using firsti
points where thej
-th segment does NOT end at pointi-1
g[i][j]
: number of ways to placej
segments using firsti
points where thej
-th segment DOES end at pointi-1
The recurrence relations handle the transitions between these states, considering whether to extend an existing segment or start a new one at each position.
Intuition
When thinking about this problem, we need to consider how line segments can be placed on a 1D line. The key insight is that at any given point, we need to make a decision: either we're continuing an existing segment or we're not part of any segment at that position.
Let's think about building our solution from left to right. At each point i
, we have two states to track:
- The last segment ends before point
i
(the point is "free") - The last segment ends exactly at point
i
(the point is an "endpoint")
This naturally leads us to define two DP states:
f[i][j]
: ways to placej
segments using points0
toi-1
, where pointi-1
is NOT the end of a segmentg[i][j]
: ways to placej
segments using points0
toi-1
, where pointi-1
IS the end of a segment
Why do we need both states? Because they transition differently:
-
If the previous point wasn't the end of a segment (
f
state), we can either:- Keep it free and move to the next point
- Start a new segment from that point
-
If the previous point was the end of a segment (
g
state), we can:- Leave it as is and move to the next point
- Extend the segment that just ended (making it longer)
The beauty of this approach is that it automatically handles non-overlapping constraints. When we're in state g
(segment just ended), we can either start fresh or extend that specific segment - we never create overlaps.
For transitions:
f[i][j] = f[i-1][j] + g[i-1][j]
: Pointi-1
is free if it was free before OR if a segment ended ati-2
g[i][j] = g[i-1][j] + f[i-1][j-1] + g[i-1][j-1]
: A segment ends ati-1
if we extend a previous segment OR start a new one
The final answer is f[n][k] + g[n][k]
since the last point can be either free or the end of a segment.
Learn more about Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The implementation uses dynamic programming with two 2D arrays to track the different states:
Data Structures:
f[i][j]
: 2D array wheref[i][j]
represents the number of ways to place exactlyj
segments using the firsti
points, with pointi-1
NOT being the endpoint of any segmentg[i][j]
: 2D array whereg[i][j]
represents the number of ways to place exactlyj
segments using the firsti
points, with pointi-1
being the endpoint of a segment
Initialization:
f[1][0] = 1
: With only 1 point and 0 segments, there's exactly 1 way (the point remains uncovered)- All other values start at 0
Transition Logic:
For each point i
from 2 to n
and for each number of segments j
from 0 to k
:
-
Calculate
f[i][j]
(pointi-1
is not an endpoint):f[i][j] = (f[i-1][j] + g[i-1][j]) % mod
This means point
i-1
is free if:- It was already free in the previous state (
f[i-1][j]
) - A segment ended at point
i-2
(g[i-1][j]
)
- It was already free in the previous state (
-
Calculate
g[i][j]
(pointi-1
is an endpoint):g[i][j] = g[i-1][j] // Extend existing segment if j > 0: g[i][j] += f[i-1][j-1] // Start new segment from free point g[i][j] += g[i-1][j-1] // Start new segment after previous ended
A segment can end at point
i-1
if:- We extend a segment that was already ending at
i-2
(g[i-1][j]
) - We start a new segment when we have
j-1
segments already placed:- From a free point (
f[i-1][j-1]
) - From a point where a segment just ended (
g[i-1][j-1]
)
- From a free point (
- We extend a segment that was already ending at
Final Answer:
return (f[n][k] + g[n][k]) % mod
The total number of ways is the sum of:
- Ways where the last point
n-1
is not part of any segment - Ways where the last point
n-1
is the endpoint of a segment
Time Complexity: O(n * k)
- We iterate through n
points and for each point, we calculate values for up to k
segments
Space Complexity: O(n * k)
- We use two 2D arrays of size (n+1) × (k+1)
The modulo operation 10^9 + 7
is applied at each step to prevent integer overflow while maintaining the correctness of the result.
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 small example with n = 4
points (at positions 0, 1, 2, 3) and k = 2
segments.
Setup:
- Points:
[0, 1, 2, 3]
- We need exactly 2 non-overlapping segments
- Initialize
f[1][0] = 1
(1 point, 0 segments = 1 way)
Building the DP tables step by step:
Step 1: i = 2 (considering points 0, 1)
f[2][0] = f[1][0] + g[1][0] = 1 + 0 = 1
(no segments)f[2][1] = f[1][1] + g[1][1] = 0 + 0 = 0
g[2][0] = g[1][0] = 0
g[2][1] = g[1][1] + f[1][0] + g[1][0] = 0 + 1 + 0 = 1
(segment [0,1])
Step 2: i = 3 (considering points 0, 1, 2)
f[3][0] = f[2][0] + g[2][0] = 1 + 0 = 1
f[3][1] = f[2][1] + g[2][1] = 0 + 1 = 1
(segment [0,1], point 2 free)f[3][2] = f[2][2] + g[2][2] = 0 + 0 = 0
g[3][0] = g[2][0] = 0
g[3][1] = g[2][1] + f[2][0] + g[2][0] = 1 + 1 + 0 = 2
- Extend [0,1] to [0,2]: 1 way
- New segment [1,2] or [2,2]: 1 way
g[3][2] = g[2][2] + f[2][1] + g[2][1] = 0 + 0 + 1 = 1
- After [0,1], add [1,2]: 1 way
Step 3: i = 4 (considering all points 0, 1, 2, 3)
f[4][2] = f[3][2] + g[3][2] = 0 + 1 = 1
g[4][2] = g[3][2] + f[3][1] + g[3][1] = 1 + 1 + 2 = 4
Final Answer: f[4][2] + g[4][2] = 1 + 4 = 5
The 5 valid configurations are:
[0,1], [2,3]
- two separate segments[0,2], [2,3]
- segments share endpoint at 2[0,1], [1,3]
- segments share endpoint at 1[0,1], [1,2]
- segments share endpoint at 1[1,2], [2,3]
- segments share endpoint at 2
This demonstrates how the DP tracks whether points are free (f
) or endpoints (g
), enabling us to count all valid non-overlapping segment placements.
Solution Implementation
1class Solution:
2 def numberOfSets(self, n: int, k: int) -> int:
3 MOD = 10**9 + 7
4
5 # dp_closed[i][j] = number of ways to partition i points into j+1 segments
6 # where the last segment is "closed" (doesn't extend to point i)
7 dp_closed = [[0] * (k + 1) for _ in range(n + 1)]
8
9 # dp_open[i][j] = number of ways to partition i points into j+1 segments
10 # where the last segment is "open" (extends to point i)
11 dp_open = [[0] * (k + 1) for _ in range(n + 1)]
12
13 # Base case: 1 point with 0 segments (1 way - the point itself)
14 dp_closed[1][0] = 1
15
16 # Fill the DP table for each number of points
17 for num_points in range(2, n + 1):
18 for num_segments in range(k + 1):
19 # A closed segment at position i can come from:
20 # 1. A closed segment at i-1 (we don't extend it)
21 # 2. An open segment at i-1 (we close it here)
22 dp_closed[num_points][num_segments] = (
23 dp_closed[num_points - 1][num_segments] +
24 dp_open[num_points - 1][num_segments]
25 ) % MOD
26
27 # An open segment at position i extends from i-1
28 dp_open[num_points][num_segments] = dp_open[num_points - 1][num_segments]
29
30 if num_segments > 0:
31 # We can also start a new open segment at position i
32 # This requires one less segment to be already formed at i-1
33 dp_open[num_points][num_segments] = (
34 dp_open[num_points][num_segments] +
35 dp_closed[num_points - 1][num_segments - 1] +
36 dp_open[num_points - 1][num_segments - 1]
37 ) % MOD
38
39 # Return the total number of ways (both closed and open final segments)
40 return (dp_closed[n][k] + dp_open[n][k]) % MOD
41
1class Solution {
2 private static final int MOD = (int) 1e9 + 7;
3
4 public int numberOfSets(int n, int k) {
5 // dp[i][j] = number of ways to form j segments using first i points
6 // where point i is NOT the end of a segment
7 int[][] dpNotEndingAtPoint = new int[n + 1][k + 1];
8
9 // dpEndingAtPoint[i][j] = number of ways to form j segments using first i points
10 // where point i IS the end of a segment
11 int[][] dpEndingAtPoint = new int[n + 1][k + 1];
12
13 // Base case: with 1 point and 0 segments, there's exactly 1 way
14 dpNotEndingAtPoint[1][0] = 1;
15
16 // Fill the DP tables for each point position
17 for (int currentPoint = 2; currentPoint <= n; ++currentPoint) {
18 for (int segmentCount = 0; segmentCount <= k; ++segmentCount) {
19 // Case 1: Current point is not the end of a segment
20 // We can transition from previous point being either end or not end
21 dpNotEndingAtPoint[currentPoint][segmentCount] =
22 (dpNotEndingAtPoint[currentPoint - 1][segmentCount] +
23 dpEndingAtPoint[currentPoint - 1][segmentCount]) % MOD;
24
25 // Case 2: Current point is the end of a segment
26 // First, extend existing segments that end at previous point
27 dpEndingAtPoint[currentPoint][segmentCount] =
28 dpEndingAtPoint[currentPoint - 1][segmentCount];
29
30 // If we have segments to form, we can create a new segment ending here
31 if (segmentCount > 0) {
32 // Start a new segment from previous point (not ending)
33 dpEndingAtPoint[currentPoint][segmentCount] =
34 (dpEndingAtPoint[currentPoint][segmentCount] +
35 dpNotEndingAtPoint[currentPoint - 1][segmentCount - 1]) % MOD;
36
37 // Extend a segment that was ending at previous point
38 dpEndingAtPoint[currentPoint][segmentCount] =
39 (dpEndingAtPoint[currentPoint][segmentCount] +
40 dpEndingAtPoint[currentPoint - 1][segmentCount - 1]) % MOD;
41 }
42 }
43 }
44
45 // Return total ways: sum of both cases at point n with k segments
46 return (dpNotEndingAtPoint[n][k] + dpEndingAtPoint[n][k]) % MOD;
47 }
48}
49
1class Solution {
2public:
3 // dp[i][j] = number of ways with i points and j segments where point i is NOT an endpoint
4 int dp[1010][1010];
5
6 // endingAt[i][j] = number of ways with i points and j segments where point i IS an endpoint
7 int endingAt[1010][1010];
8
9 const int MOD = 1e9 + 7;
10
11 int numberOfSets(int n, int k) {
12 // Initialize DP tables
13 memset(dp, 0, sizeof(dp));
14 memset(endingAt, 0, sizeof(endingAt));
15
16 // Base case: 1 point, 0 segments - one valid way
17 dp[1][0] = 1;
18
19 // Fill DP table for each point from 2 to n
20 for (int points = 2; points <= n; ++points) {
21 for (int segments = 0; segments <= k; ++segments) {
22 // Case 1: Current point is not an endpoint
23 // We can take all configurations from previous point
24 dp[points][segments] = (dp[points - 1][segments] + endingAt[points - 1][segments]) % MOD;
25
26 // Case 2: Current point is an endpoint of a segment
27 // First, extend existing segments that end at previous point
28 endingAt[points][segments] = endingAt[points - 1][segments];
29
30 if (segments > 0) {
31 // Start a new segment ending at current point
32 // Can start from any previous configuration with (segments - 1) segments
33 endingAt[points][segments] = (endingAt[points][segments] + dp[points - 1][segments - 1]) % MOD;
34
35 // Or extend a segment that was ending at previous point
36 endingAt[points][segments] = (endingAt[points][segments] + endingAt[points - 1][segments - 1]) % MOD;
37 }
38 }
39 }
40
41 // Return total: configurations where last point is or isn't an endpoint
42 return (dp[n][k] + endingAt[n][k]) % MOD;
43 }
44};
45
1/**
2 * Counts the number of ways to draw k non-overlapping line segments on n points
3 * @param n - The number of points on a line
4 * @param k - The number of line segments to draw
5 * @returns The number of ways modulo 10^9 + 7
6 */
7function numberOfSets(n: number, k: number): number {
8 // dp[i][j] represents the number of ways to draw j segments using first i points
9 // where the last segment does NOT end at point i
10 const dpNotEnding: number[][] = Array.from(
11 { length: n + 1 },
12 () => new Array(k + 1).fill(0)
13 );
14
15 // dp[i][j] represents the number of ways to draw j segments using first i points
16 // where the last segment DOES end at point i
17 const dpEnding: number[][] = Array.from(
18 { length: n + 1 },
19 () => new Array(k + 1).fill(0)
20 );
21
22 // Base case: with 1 point and 0 segments, there's 1 way (empty set)
23 dpNotEnding[1][0] = 1;
24
25 const MOD: number = 10 ** 9 + 7;
26
27 // Iterate through each point position
28 for (let i = 2; i <= n; ++i) {
29 // Iterate through each possible number of segments
30 for (let j = 0; j <= k; ++j) {
31 // Ways where j segments don't end at point i =
32 // ways from previous point (either ending or not ending there)
33 dpNotEnding[i][j] = (dpNotEnding[i - 1][j] + dpEnding[i - 1][j]) % MOD;
34
35 // Extend segments that ended at point i-1 to point i
36 dpEnding[i][j] = dpEnding[i - 1][j];
37
38 if (j > 0) {
39 // Start a new segment at point i (from any previous configuration with j-1 segments)
40 dpEnding[i][j] = (dpEnding[i][j] + dpNotEnding[i - 1][j - 1]) % MOD;
41
42 // Extend a segment that was already ending at i-1 with j-1 segments
43 dpEnding[i][j] = (dpEnding[i][j] + dpEnding[i - 1][j - 1]) % MOD;
44 }
45 }
46 }
47
48 // Return total ways: segments either ending or not ending at point n
49 return (dpNotEnding[n][k] + dpEnding[n][k]) % MOD;
50}
51
Time and Space Complexity
Time Complexity: O(n * k)
The algorithm uses nested loops where:
- The outer loop iterates from
2
ton + 1
, which gives usn - 1
iterations - The inner loop iterates from
0
tok
, which gives usk + 1
iterations - Inside the inner loop, all operations (additions and modulo operations) are
O(1)
Therefore, the total time complexity is O((n - 1) * (k + 1))
which simplifies to O(n * k)
.
Space Complexity: O(n * k)
The algorithm creates two 2D arrays:
- Array
f
with dimensions(n + 1) × (k + 1)
- Array
g
with dimensions(n + 1) × (k + 1)
Each array requires O((n + 1) * (k + 1))
space, and since we have two such arrays, the total space complexity is O(2 * (n + 1) * (k + 1))
, which simplifies to O(n * k)
.
Note: The space complexity could potentially be optimized to O(k)
since each iteration only depends on the previous row, allowing us to use rolling arrays instead of storing all rows.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Points with Segments
A critical misunderstanding is thinking that n
points can form n-1
segments at most. While this is true for consecutive single-unit segments, segments can span multiple points. For example, with 4 points (0,1,2,3), segment [0,3] covers all 4 points but counts as just 1 segment.
Solution: Remember that a segment from point i
to point j
covers all points in between, and the maximum number of non-overlapping segments depends on how you partition the points, not just the gaps between consecutive points.
2. Incorrect Base Case Initialization
Many implementations incorrectly initialize dp_closed[0][0] = 1
or dp_open[0][0] = 1
, thinking that 0 points with 0 segments is a valid configuration.
Solution: The correct base case is dp_closed[1][0] = 1
because you need at least 1 point to have a valid configuration. With 1 point and 0 segments, there's exactly one way (leave the point uncovered).
3. Off-by-One Errors in Array Indexing
The problem uses 0-indexed points (0 to n-1) but the DP arrays often use 1-indexed counting for the number of points. Mixing these can lead to accessing wrong indices or missing edge cases.
Solution: Be consistent with your indexing scheme. In the solution, dp[i][j]
represents using i
points (points 0 through i-1), not point index i
.
4. Forgetting Modulo Operations
Missing modulo operations at any step can cause integer overflow, especially for large values of n
and k
. This leads to incorrect results even if the logic is correct.
Solution: Apply modulo after every addition operation:
# Wrong dp_closed[i][j] = dp_closed[i-1][j] + dp_open[i-1][j] result = result % MOD # Too late! # Correct dp_closed[i][j] = (dp_closed[i-1][j] + dp_open[i-1][j]) % MOD
5. Misunderstanding State Transitions
A common error is thinking that an "open" segment at position i
must start at position i
. Actually, an open segment at position i
means it ends at or extends through position i-1
.
Solution: Clearly define what each state represents:
dp_closed[i][j]
: Using points 0 to i-1, with j segments, where no segment ends at point i-1dp_open[i][j]
: Using points 0 to i-1, with j segments, where a segment ends at point i-1
6. Not Handling the Minimum Segment Length
Forgetting that each segment must cover at least 2 points. A segment cannot be a single point.
Solution: Ensure that when starting a new segment, there's room for it to extend to at least one more point. This is implicitly handled in the DP transitions but should be kept in mind when understanding the problem.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!