1024. Video Stitching
Problem Description
You have a collection of video clips from a sporting event that lasted time
seconds. Each clip is represented as [start, end]
, where start
is when the clip begins and end
is when it ends. These clips can overlap with each other and have different lengths.
Your task is to select the minimum number of clips needed to cover the entire event from second 0
to second time
. You can use any portion of a clip - for example, a clip [0, 7]
can be cut and used as segments like [0, 1]
, [1, 3]
, and [3, 7]
.
Given:
- An array
clips
whereclips[i] = [start_i, end_i]
represents the i-th clip - An integer
time
representing the duration of the event
Return:
- The minimum number of clips needed to cover
[0, time]
- Return
-1
if it's impossible to cover the entire duration
For example, if you have clips [[0,2], [1,5], [3,7]]
and time = 6
, you could use clips [0,2]
and [1,5]
to cover [0,5]
, then use [3,7]
to cover the remaining time, requiring 3 clips total. However, a better solution would be to use [0,2]
and [1,5]
(which covers up to 5) and [3,7]
(which covers from 3 to 7), but since [1,5]
already reaches position 5, we only need 2 clips: [0,2]
extended by [1,5]
to cover [0,5]
, and then [3,7]
to complete the coverage.
The key insight is that you want to greedily select clips that extend your coverage as far as possible while ensuring no gaps exist in the coverage from 0
to time
.
Intuition
Think of this problem like trying to build a bridge across a river using planks of different lengths. You want to use the minimum number of planks to get from one side (position 0
) to the other (position time
).
The key observation is that at each position, we want to choose the clip that can take us the farthest. If multiple clips start at the same position, we should always pick the one that extends the furthest - there's no benefit to choosing a shorter clip when a longer one is available from the same starting point.
This leads us to a preprocessing step: for each position i
, we can calculate the farthest position reachable from clips starting at i
. We store this in an array last[i]
.
Now imagine walking along the timeline from position 0
. At each step, we keep track of:
mx
: the farthest position we can reach using all clips we've seen so farpre
: the right boundary of the last clip we actually selectedans
: the count of clips we've used
As we walk forward, we continuously update mx
with the farthest reachable position. When we reach the boundary of our current clip (when i == pre
), we must select a new clip. The best choice is to jump to the farthest position we can reach (mx
), incrementing our clip count.
If at any point mx <= i
, it means we can't move forward anymore - there's a gap in coverage that makes it impossible to reach time
.
This greedy strategy works because once we've decided to use a clip to cover up to position pre
, the optimal choice for the next clip is always the one that extends the furthest from any position we've already covered. There's never a benefit to choosing a shorter extension when a longer one is available.
The approach is similar to the "Jump Game" problem where you want to reach the end with minimum jumps - at each forced jump point, you jump to the farthest position possible based on all the positions you've explored.
Learn more about Greedy and Dynamic Programming patterns.
Solution Approach
The implementation follows a greedy approach with preprocessing to optimize clip selection.
Step 1: Preprocessing - Build the last
array
We create an array last
of size time
where last[i]
stores the farthest position reachable from any clip starting at position i
.
last = [0] * time
for a, b in clips:
if a < time:
last[a] = max(last[a], b)
For each clip [a, b]
, if the start position a
is within our range [0, time)
, we update last[a]
to be the maximum of its current value and b
. This ensures that for clips with the same starting point, we only keep track of the one with the farthest endpoint.
Step 2: Greedy Traversal
We initialize three variables:
mx = 0
: tracks the farthest position reachable from all positions we've seenpre = 0
: marks the right boundary of the last selected clipans = 0
: counts the number of clips used
ans = mx = pre = 0
for i, v in enumerate(last):
mx = max(mx, v)
if mx <= i:
return -1
if pre == i:
ans += 1
pre = mx
We iterate through each position i
from 0
to time-1
:
-
Update maximum reach:
mx = max(mx, last[i])
- we update the farthest reachable position considering the clip starting at positioni
. -
Check for gaps: If
mx <= i
, it means we cannot go beyond the current positioni
. This creates a gap in coverage, making it impossible to reachtime
, so we return-1
. -
Select new clip: When
pre == i
, we've reached the boundary of our currently selected clip and must choose a new one. We:- Increment
ans
to count this new clip - Update
pre = mx
to jump to the farthest position we can reach
- Increment
The algorithm essentially creates "jump points" where we must select a new clip, and at each jump point, we greedily choose to extend as far as possible.
Time Complexity: O(n + m)
where n
is the number of clips and m
is the value of time
O(n)
for preprocessing the clipsO(m)
for the traversal
Space Complexity: O(m)
for the last
array
This approach guarantees the minimum number of clips because at each forced selection point, we're making the optimal choice by extending as far as possible, eliminating the need to backtrack or consider alternative paths.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with clips = [[0,2], [1,6], [3,5], [5,7]]
and time = 6
.
Step 1: Build the last
array
We create last = [0, 0, 0, 0, 0, 0]
(size 6 for positions 0-5).
Processing each clip:
[0,2]
: Setlast[0] = 2
(from position 0, we can reach position 2)[1,6]
: Setlast[1] = 6
(from position 1, we can reach position 6)[3,5]
: Setlast[3] = 5
(from position 3, we can reach position 5)[5,7]
: Setlast[5] = 7
(from position 5, we can reach position 7)
Result: last = [2, 6, 0, 5, 0, 7]
Step 2: Greedy Traversal
Initialize: ans = 0
, mx = 0
, pre = 0
Position i | last[i] | mx (after update) | Check mx ≤ i? | Check pre == i? | Action | ans | pre |
---|---|---|---|---|---|---|---|
0 | 2 | max(0,2) = 2 | 2 > 0 ✓ | 0 == 0 ✓ | Select new clip | 1 | 2 |
1 | 6 | max(2,6) = 6 | 6 > 1 ✓ | 2 ≠ 1 ✗ | Continue | 1 | 2 |
2 | 0 | max(6,0) = 6 | 6 > 2 ✓ | 2 == 2 ✓ | Select new clip | 2 | 6 |
3 | 5 | max(6,5) = 6 | 6 > 3 ✓ | 6 ≠ 3 ✗ | Continue | 2 | 6 |
4 | 0 | max(6,0) = 6 | 6 > 4 ✓ | 6 ≠ 4 ✗ | Continue | 2 | 6 |
5 | 7 | max(6,7) = 7 | 7 > 5 ✓ | 6 ≠ 5 ✗ | Continue | 2 | 6 |
After the loop, pre = 6 ≥ time = 6
, so we've successfully covered the entire duration.
Result: Return ans = 2
The algorithm selected 2 clips:
- First selection at position 0: Used a clip to cover
[0,2]
- Second selection at position 2: Jumped to position 6 (using the clip starting at position 1)
This gives us coverage from 0 to 6 with just 2 clips, which is optimal.
Solution Implementation
1from typing import List
2
3class Solution:
4 def videoStitching(self, clips: List[List[int]], time: int) -> int:
5 # Store the furthest end point reachable from each starting position
6 furthest_reach = [0] * time
7
8 # Build the furthest reach array from all clips
9 for start, end in clips:
10 # Only consider clips that start before the target time
11 if start < time:
12 furthest_reach[start] = max(furthest_reach[start], end)
13
14 # Initialize variables for the greedy algorithm
15 num_clips = 0 # Number of clips used
16 max_reach = 0 # Maximum position we can reach so far
17 prev_end = 0 # End position of the last selected clip
18
19 # Iterate through each position in the time interval
20 for current_pos, furthest_from_here in enumerate(furthest_reach):
21 # Update the maximum reachable position
22 max_reach = max(max_reach, furthest_from_here)
23
24 # If we can't reach beyond current position, there's a gap
25 if max_reach <= current_pos:
26 return -1
27
28 # If we've reached the end of the previous clip's coverage
29 if prev_end == current_pos:
30 # Select a new clip that extends furthest
31 num_clips += 1
32 prev_end = max_reach
33
34 return num_clips
35
1class Solution {
2 public int videoStitching(int[][] clips, int time) {
3 // Array to store the farthest point each position can reach
4 // last[i] represents the maximum end time that can be reached from position i
5 int[] farthestReach = new int[time];
6
7 // Preprocess clips to find the farthest reach from each starting position
8 for (int[] clip : clips) {
9 int startTime = clip[0];
10 int endTime = clip[1];
11
12 // Only consider clips that start before our target time
13 if (startTime < time) {
14 farthestReach[startTime] = Math.max(farthestReach[startTime], endTime);
15 }
16 }
17
18 // Variables for greedy algorithm
19 int numClips = 0; // Number of clips used
20 int maxReachable = 0; // Maximum position reachable so far
21 int currentIntervalEnd = 0; // End of current selected interval
22
23 // Iterate through each time point to find minimum clips needed
24 for (int currentTime = 0; currentTime < time; currentTime++) {
25 // Update the maximum reachable position from current and previous positions
26 maxReachable = Math.max(maxReachable, farthestReach[currentTime]);
27
28 // If we cannot reach beyond current position, it's impossible to cover
29 if (maxReachable <= currentTime) {
30 return -1;
31 }
32
33 // If we've reached the end of our current interval, select a new clip
34 if (currentIntervalEnd == currentTime) {
35 numClips++;
36 currentIntervalEnd = maxReachable;
37 }
38 }
39
40 return numClips;
41 }
42}
43
1class Solution {
2public:
3 int videoStitching(vector<vector<int>>& clips, int time) {
4 // Create an array to store the farthest end point that can be reached from each start point
5 vector<int> farthestReach(time);
6
7 // Process each clip to update the farthest reach from each starting position
8 for (auto& clip : clips) {
9 int startTime = clip[0];
10 int endTime = clip[1];
11
12 // Only consider clips that start before the target time
13 if (startTime < time) {
14 farthestReach[startTime] = max(farthestReach[startTime], endTime);
15 }
16 }
17
18 // Variables for greedy algorithm
19 int maxReachable = 0; // Maximum time that can be reached so far
20 int clipCount = 0; // Number of clips used
21 int currentEndpoint = 0; // End point of the current selected clip
22
23 // Iterate through each time point to find minimum clips needed
24 for (int currentTime = 0; currentTime < time; ++currentTime) {
25 // Update the maximum reachable time from current position
26 maxReachable = max(maxReachable, farthestReach[currentTime]);
27
28 // If we cannot cover the current time point, it's impossible
29 if (maxReachable <= currentTime) {
30 return -1;
31 }
32
33 // If we've reached the end of our current clip, we need a new one
34 if (currentEndpoint == currentTime) {
35 ++clipCount;
36 currentEndpoint = maxReachable; // Extend coverage to the farthest point possible
37 }
38 }
39
40 return clipCount;
41 }
42};
43
1function videoStitching(clips: number[][], time: number): number {
2 // Create an array to store the farthest end point that can be reached from each start point
3 const farthestReach: number[] = new Array(time).fill(0);
4
5 // Process each clip to update the farthest reach from each starting position
6 for (const clip of clips) {
7 const startTime = clip[0];
8 const endTime = clip[1];
9
10 // Only consider clips that start before the target time
11 if (startTime < time) {
12 farthestReach[startTime] = Math.max(farthestReach[startTime], endTime);
13 }
14 }
15
16 // Variables for greedy algorithm
17 let maxReachable = 0; // Maximum time that can be reached so far
18 let clipCount = 0; // Number of clips used
19 let currentEndpoint = 0; // End point of the current selected clip
20
21 // Iterate through each time point to find minimum clips needed
22 for (let currentTime = 0; currentTime < time; currentTime++) {
23 // Update the maximum reachable time from current position
24 maxReachable = Math.max(maxReachable, farthestReach[currentTime]);
25
26 // If we cannot cover the current time point, it's impossible
27 if (maxReachable <= currentTime) {
28 return -1;
29 }
30
31 // If we've reached the end of our current clip, we need a new one
32 if (currentEndpoint === currentTime) {
33 clipCount++;
34 currentEndpoint = maxReachable; // Extend coverage to the farthest point possible
35 }
36 }
37
38 return clipCount;
39}
40
Time and Space Complexity
Time Complexity: O(n + T)
where n
is the number of clips and T
is the time value.
- The first loop iterates through all
n
clips to populate thelast
array, takingO(n)
time - The second loop iterates through the
last
array of sizetime
, takingO(T)
time - Overall time complexity is
O(n + T)
Space Complexity: O(T)
where T
is the time value.
- The algorithm creates a
last
array of sizetime
to store the maximum reachable position from each starting point - Only constant extra variables (
ans
,mx
,pre
) are used besides thelast
array - Overall space complexity is
O(T)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Handling Clips That Start Beyond the Target Time
The Problem:
The current implementation only checks if start < time
when building the furthest_reach
array, but this creates an issue when we have clips that start at exactly time
or beyond. While these clips shouldn't be included in our solution, the array initialization with size time
means we're not tracking the furthest reach from position time-1
correctly if there's a clip starting there.
Example:
clips = [[0, 1], [1, 2]] time = 2
The furthest_reach
array has size 2 (indices 0 and 1), but we never check if we can actually reach position 2 from position 1.
Solution:
Ensure we properly track whether we can reach the target time
:
def videoStitching(self, clips: List[List[int]], time: int) -> int:
if time == 0:
return 0
# Store the furthest end point reachable from each starting position
furthest_reach = [0] * (time + 1) # Include one extra position
for start, end in clips:
if start <= time: # Changed from < to <=
furthest_reach[start] = max(furthest_reach[start], end)
num_clips = 0
max_reach = 0
prev_end = 0
for current_pos in range(time): # Explicit range instead of enumerate
max_reach = max(max_reach, furthest_reach[current_pos])
if max_reach <= current_pos:
return -1
if prev_end == current_pos:
num_clips += 1
prev_end = max_reach
# Early termination if we can already reach the target
if prev_end >= time:
return num_clips
# Check if we can reach the target time
return num_clips if prev_end >= time else -1
Pitfall 2: Incorrect Handling of Edge Case When time = 0
The Problem:
When time = 0
, we don't need any clips to cover the interval [0, 0]
. However, the current implementation creates an empty array for furthest_reach
which causes the loop to not execute at all, returning 0 by default. While this gives the correct answer, it's by accident rather than design.
Solution: Add explicit handling for this edge case at the beginning:
def videoStitching(self, clips: List[List[int]], time: int) -> int:
if time == 0:
return 0 # No clips needed to cover empty interval
# Rest of the implementation...
Pitfall 3: Not Verifying Final Coverage
The Problem:
The current implementation assumes that if we can traverse through all positions from 0 to time-1
without gaps, we've successfully covered the entire interval. However, it doesn't explicitly verify that the last selected clip actually reaches or exceeds time
.
Example:
clips = [[0, 2], [1, 3], [2, 4]] time = 5
The algorithm might select clips that cover up to position 4, but never actually reach position 5.
Solution: Add a final check after the loop:
def videoStitching(self, clips: List[List[int]], time: int) -> int:
# ... previous code ...
for current_pos in range(time):
max_reach = max(max_reach, furthest_reach[current_pos])
if max_reach <= current_pos:
return -1
if prev_end == current_pos:
num_clips += 1
prev_end = max_reach
# Verify that we can actually reach the target time
return num_clips if max_reach >= time else -1
Complete Corrected Solution:
def videoStitching(self, clips: List[List[int]], time: int) -> int:
if time == 0:
return 0
# Store the furthest end point reachable from each starting position
furthest_reach = [0] * time
for start, end in clips:
if 0 <= start < time:
furthest_reach[start] = max(furthest_reach[start], end)
num_clips = 0
max_reach = 0
prev_end = 0
for current_pos in range(time):
max_reach = max(max_reach, furthest_reach[current_pos])
# Can't reach beyond current position - gap in coverage
if max_reach <= current_pos:
return -1
# Need to select a new clip
if prev_end == current_pos:
num_clips += 1
prev_end = max_reach
# Early termination if we've covered the target
if prev_end >= time:
return num_clips
# Final check if we've covered the entire duration
return num_clips if prev_end >= time else -1
Which of the following is a min heap?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!