1024. Video Stitching
Problem Description
You are tasked with the challenge of creating a highlight reel from a series of overlapping video clips taken during a sporting event. The entire event has a duration of time
seconds. The video clips are provided in the form of a list, where each clip is represented by a pair of numbers: the start time (start_i
) and the end time (end_i
). The clips may overlap and can have different lengths. Interestingly, you have the freedom to cut these clips into any number of segments you wish.
The main objective is to find the smallest number of clips necessary to cover the entire duration of the event from 0 to time
seconds. If it is not possible to cover the entire event with the given clips, the function should return -1.
Intuition
To solve this problem, we first need to think about how to use the given clips to cover the entire time span of the event. A brute force approach might try to consider all possible combinations of clips, but this would be highly inefficient. Instead, we need a smarter strategy that can quickly determine the optimal set of clips without redundant computations.
The intuition behind the solution is to first figure out, for each second in the event's duration, what is the furthest point we can reach if we were to start a clip at that second. We begin by initializing an array that will hold this information. As we iterate through each clip, we record the farthest end time we can achieve, starting from each start time that is less than time
.
Next, we iterate through each second in the event, extending our reach as we find greater end times. The goal is to keep extending our current segment's end time (pre
) until we can no longer do so or until we reach the end of the event. If at any point, the farthest reach (mx
) is less than or equal to the current second, it means there is a gap in the coverage and hence the task is impossible.
On the other hand, when the current second reaches the previous maximum reach (pre
), it indicates that we have utilized a clip to its fullest extent, and we need to select another clip to extend our coverage. This selection increments our answer to the minimum number of clips needed. Eventually, if we are able to cover the whole event, we return the number of clips used; otherwise, if we encounter a coverage gap, we return -1.
This strategy is efficient because it takes advantage of the greedy approach, always picking the clip that extends coverage the furthest at each step, and ensuring that a minimum number of clips are used. The algorithm will sweep through the time units only once, maintaining the maximum reach and updating the number of clips used as needed.
Learn more about Greedy and Dynamic Programming patterns.
Solution Approach
The solution involves a greedy algorithm that aims to extend the reach of the current clip segment as far as possible within the given time. Let's walk through the implementation:
-
First, we initialize a list called
last
with a length equal to thetime
. This list is used to track the furthest end time (last[a]
) that can be reached from each starting seconda
. -
The solution iterates over each clip
[a, b]
from the providedclips
list. For each clip, it checks if the start timea
is within the event duration (a < time
). If so, it updateslast[a]
with the maximum of its current value and the end timeb
of the current clip. This step effectively finds the furthest end time achievable from each start time.
last = [0] * time
for a, b in clips:
if a < time:
last[a] = max(last[a], b)
-
We then initialize three variables:
ans
to keep track of the number of clips used,mx
to store the furthest end time reachable at any point, andpre
which represents the end of the current clip segment we are creating. -
The next loop goes through each second (index
i
) in thelast
list, updatingmx
to the maximum of its current value andlast[i]
which represents the furthest time we can reach from secondi
. Here are two important conditions:-
If at any point
mx
(the maximum reach so far) is less than or equal to the current secondi
, this means that there is a gap in our clip coverage and we cannot create a continuous highlight reel up to this second. Hence, we return-1
. -
If the current second
i
equalspre
(the previous segment's farthest reach), it means that we need to start a new segment by selecting another clip. We incrementans
(the number of clips used), and updatepre
tomx
to reflect the new segment's farthest reach.
-
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
- After the loop is completed, if we are able to reach the end of the event's duration (
time
), the variableans
will hold the minimum number of clips needed. The function returns this value.
This approach uses a greedy algorithm for selecting clips, as well as dynamic programming techniques to efficiently calculate the maximum coverage at each point in time. By continuously updating the furthest reach and tracking the number of clips, the algorithm ensures that it always progresses towards the goal and only uses the necessary clips to cover the entire event duration.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example. Suppose we have an event that lasts for 10 seconds (time = 10
), and we have the following video clips provided in the form of (start_i, end_i)
pairs:
Clips = [(0, 3), (1, 5), (4, 8), (7, 10)]
Following the solution approach:
-
Initialize the
last
list with a length equal totime
(which is 10 in this case), to track the furthest end time we can reach from each starting second.last = [0]*10 # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
-
Iterate through each clip to update the
last
list.- For clip
(0, 3)
, since0 < 10
, updatelast[0]
to 3. - For clip
(1, 5)
, since1 < 10
, updatelast[1]
to 5. - For clip
(4, 8)
, since4 < 10
, updatelast[4]
to 8. - For clip
(7, 10)
, since7 < 10
, updatelast[7]
to 10.
After iterating,
last
looks like this:last = [3, 5, 0, 0, 8, 0, 0, 10, 0, 0]
- For clip
-
Initialize
ans
(the number of clips used),mx
(the farthest end time reachable at any point), andpre
(the end of the current clip segment) to 0. -
Iterate through the
last
list to find the minimum number of clips:- For
i = 0
,mx
becomes max(0, 3), somx = 3
. - For
i = 1
,mx
becomes max(3, 5), somx = 5
. - Since
i
has not reachedpre
yet, we continue without incrementingans
. - For
i = 2
,mx
remains 5 aslast[2]
is 0 and5 > 2
. - ...
- When
i = 3
andpre = 3
, we find our first segment covering from second 0 to second 5. Incrementans
to 1, and updatepre
tomx
(5). - Continue this process until
i=7
. - For
i = 7
,mx
becomes max(5, 10), somx = 10
. i = 7
is equal topre
which is 5, so incrementans
to 2, and updatepre
tomx
(10).- We now have covered the duration from 0 to 10 seconds with 2 segments.
- For
-
Finally, since the
last
list has been fully traversed and we've been able to cover every second up to the end of the event (10
), we conclude that 2 is the minimum number of clips needed to cover the event.
Following this example, the function would return 2
since we can cover the entire event with the [(1, 5), (7, 10)] clips.
Solution Implementation
1from typing import List
2
3class Solution:
4 def videoStitching(self, clips: List[List[int]], target_time: int) -> int:
5 # Initialize a list to keep track of the furthest point each second can reach
6 furthest_reach = [0] * target_time
7
8 # Pre-process the clips to fill in the furthest_reach list
9 for start, end in clips:
10 # Only consider clips that start before the target_time
11 if start < target_time:
12 # Update the furthest_reach for the second that this clip starts
13 furthest_reach[start] = max(furthest_reach[start], end)
14
15 clips_required = 0 # Counter for the minimum number of clips required
16 current_end = 0 # The furthest point we can reach without adding another clip
17 next_end = 0 # The furthest point we can reach by including another clip
18
19 # Iterate through each second up to the target_time
20 for second in range(target_time):
21 # Determine the most distant point in time we can reach from this second
22 next_end = max(next_end, furthest_reach[second])
23
24 # If the next_end is less or equal to the current second, it means we have a gap.
25 # We can't reach the current second from any previous clips.
26 if next_end <= second:
27 return -1
28
29 # When the current second reaches the current_end, we need to select a new clip
30 if current_end == second:
31 clips_required += 1
32 # Update current_end to the furthest point reachable from this clip
33 current_end = next_end
34
35 return clips_required
36
37# Example Usage
38solution = Solution()
39clips = [[0,2], [4,6], [8,10], [1,9], [1,5], [5,9]]
40target_time = 10
41print(solution.videoStitching(clips, target_time)) # Outputs the minimum number of clips needed
42
1class Solution {
2 public int videoStitching(int[][] clips, int T) {
3 int[] maxReach = new int[T];
4
5 // Iterate over each clip and record the furthest end time for each start time
6 for (int[] clip : clips) {
7 int start = clip[0], end = clip[1];
8 if (start < T) {
9 maxReach[start] = Math.max(maxReach[start], end);
10 }
11 }
12
13 int count = 0; // the minimum number of clips needed
14 int maxEnd = 0; // the farthest end time we can reach so far
15 int prevEnd = 0; // the end time of the last clip we have included in the solution
16
17 // Loop through each time unit up to T
18 for (int i = 0; i < T; i++) {
19 maxEnd = Math.max(maxEnd, maxReach[i]);
20
21 // If the maxEnd we can reach is less or equal to current time 'i',
22 // it is impossible to stitch the video up to 'i'
23 if (maxEnd <= i) {
24 return -1;
25 }
26
27 // When we reach the end of the previous clip, increment the count of clips
28 // and set the prevEnd to maxEnd to try and reach further in the next iteration
29 if (prevEnd == i) {
30 count++;
31 prevEnd = maxEnd;
32 }
33 }
34
35 // Return the minimum number of clips needed
36 return count;
37 }
38}
39
1class Solution {
2public:
3 int videoStitching(vector<vector<int>>& clips, int time) {
4 // lastSeen represents the farthest time we can get to starting from each second
5 vector<int> lastSeen(time, 0);
6
7 // Iterate through the clips and fill up the lastSeen vector with the farthest end time
8 // for clips that start at or before 'i' and are relevant to our goal (i.e., within 'time').
9 for (auto& clip : clips) {
10 int start = clip[0], end = clip[1];
11 if (start < time) {
12 lastSeen[start] = max(lastSeen[start], end);
13 }
14 }
15
16 // 'maxReach' holds the farthest time we can reach at any moment
17 int maxReach = 0;
18 // 'answer' is the minimum number of clips needed to cover [0, time)
19 int answer = 0;
20 // 'previous' holds the end time of the last selected clip
21 int previous = 0;
22
23 // Iterate from 0 to time to determine if we can reach 'time' and what minimum number of clips are needed
24 for (int i = 0; i < time; ++i) {
25 // Extend the maxReach if the current second allows for a farther reach
26 maxReach = max(maxReach, lastSeen[i]);
27
28 // If maxReach is less or equal to 'i', it means we can't move past this second, thus return -1
29 if (maxReach <= i) {
30 return -1;
31 }
32
33 // If 'i' equals 'previous', it means we've used one clip and need to select the next
34 // The selection is based on how far we can reach from here (maxReach)
35 if (previous == i) {
36 ++answer; // Increment the number of clips used
37 previous = maxReach; // Update 'previous' to be the farthest time we can reach now
38 }
39 }
40
41 // Return the minimum number of clips needed
42 return answer;
43 }
44};
45
1// Defines the type for a video clip, which is an array of 2 numbers
2type Clip = [number, number];
3
4// Initializes an array to represent the farthest time reachable starting from each second
5let lastSeen: number[] = [];
6
7// Calculates the minimum number of clips required to cover a range from 0 to time
8// clips: an array of video clips defined by their start and end times
9// time: the total time that needs to be covered by the clips
10function videoStitching(clips: Clip[], time: number): number {
11 // Reset the lastSeen array for the current computation
12 lastSeen = Array(time).fill(0);
13
14 // Populate the lastSeen array with the furthest end time for relevant clips
15 for (let clip of clips) {
16 let [start, end] = clip;
17 if (start < time) {
18 lastSeen[start] = Math.max(lastSeen[start], end);
19 }
20 }
21
22 // Variables to track the farthest time we can reach and the minimum clips needed
23 let maxReach = 0;
24 let answer = 0;
25 let previousReach = 0;
26
27 // Iterate through each second until time to compute the minimum number of clips required
28 for (let i = 0; i < time; i++) {
29 // Extend the maxReach with the furthest time we can achieve from this second
30 maxReach = Math.max(maxReach, lastSeen[i]);
31
32 // If we cannot progress beyond this second, return -1 to indicate it's impossible
33 if (maxReach <= i) {
34 return -1;
35 }
36
37 // If we've reached the end of the current clip, select the next clip
38 if (previousReach == i) {
39 answer++;
40 previousReach = maxReach; // Update our reach to the furthest end time encountered
41 }
42 }
43
44 // Return the calculated minimum number of clips needed
45 return answer;
46}
47
Time and Space Complexity
Time Complexity
The provided Python code has a for loop that iterates through the clips
list, which has a length that we can refer to as n
. Inside this loop, we have constant time operations (if
comparison and max
function), meaning that this part of the algorithm has a time complexity of O(n)
.
Following that, there is another for loop which iterates through the last
list. Since last
has a length equal to the time
parameter, this loop iterates time
times. Once again, we perform constant time operations within the loop (max
function, if
comparisons, and simple assignments).
Therefore, the second loop has a time complexity of O(time)
. Since these two loops are not nested but executed in sequence, the overall time complexity of the code combines both complexities, resulting in O(n + time)
.
Space Complexity
For space complexity, the code allocates an array last
of length equal to time
, which requires O(time)
space. The rest of the variables used in the code (ans
, mx
, and pre
) require constant space, O(1)
.
Therefore, the overall space complexity of the function is O(time)
because this is the largest space requirement that does not change with different inputs of clips
.
Learn more about how to find time and space complexity quickly using problem constraints.
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!