Facebook Pixel

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 where clips[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 far
  • pre: the right boundary of the last clip we actually selected
  • ans: 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 seen
  • pre = 0: marks the right boundary of the last selected clip
  • ans = 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:

  1. Update maximum reach: mx = max(mx, last[i]) - we update the farthest reachable position considering the clip starting at position i.

  2. Check for gaps: If mx <= i, it means we cannot go beyond the current position i. This creates a gap in coverage, making it impossible to reach time, so we return -1.

  3. 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

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 clips
  • O(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 Evaluator

Example 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]: Set last[0] = 2 (from position 0, we can reach position 2)
  • [1,6]: Set last[1] = 6 (from position 1, we can reach position 6)
  • [3,5]: Set last[3] = 5 (from position 3, we can reach position 5)
  • [5,7]: Set last[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 ilast[i]mx (after update)Check mx ≤ i?Check pre == i?Actionanspre
02max(0,2) = 22 > 0 ✓0 == 0 ✓Select new clip12
16max(2,6) = 66 > 1 ✓2 ≠ 1 ✗Continue12
20max(6,0) = 66 > 2 ✓2 == 2 ✓Select new clip26
35max(6,5) = 66 > 3 ✓6 ≠ 3 ✗Continue26
40max(6,0) = 66 > 4 ✓6 ≠ 4 ✗Continue26
57max(6,7) = 77 > 5 ✓6 ≠ 5 ✗Continue26

After the loop, pre = 6 ≥ time = 6, so we've successfully covered the entire duration.

Result: Return ans = 2

The algorithm selected 2 clips:

  1. First selection at position 0: Used a clip to cover [0,2]
  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 the last array, taking O(n) time
  • The second loop iterates through the last array of size time, taking O(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 size time to store the maximum reachable position from each starting point
  • Only constant extra variables (ans, mx, pre) are used besides the last 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More