Maximize profit from non-overlapping jobs
Company: Airbnb
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Onsite
Quick Answer: This question evaluates proficiency in weighted interval scheduling, dynamic programming concepts, and the use of efficient data structures for large-scale scheduling problems, including handling duplicate times, large reward values, and memory constraints.
Constraints
- 0 <= len(jobs) <= 200000
- 0 <= start < end <= 10^9 for every job
- 1 <= reward <= 10^9 for every job
- The total reward may exceed 32-bit signed integer range
Examples
Input: ([],)
Expected Output: (0, [])
Explanation: With no jobs, the maximum reward is 0 and the chosen set is empty.
Input: ([(1, 3, 50), (3, 5, 20), (6, 19, 100), (2, 100, 200)],)
Expected Output: (200, [3])
Explanation: Taking job 3 alone gives reward 200, which is better than chaining jobs 0, 1, and 2 for 170.
Input: ([(1, 3, 20), (1, 3, 30), (3, 5, 25), (3, 6, 50), (5, 6, 30)],)
Expected Output: (85, [1, 2, 4])
Explanation: Jobs 1, 2, and 4 do not overlap and give 30 + 25 + 30 = 85, which is optimal.
Input: ([(1, 2, 5), (2, 3, 5), (1, 3, 10)],)
Expected Output: (10, [2])
Explanation: Both [0, 1] and [2] give reward 10. After sorting by (end, start, index) and preferring skip on ties, the deterministic answer is [2].
Input: ([(4, 6, 5), (1, 3, 5), (2, 5, 6), (6, 7, 4), (5, 8, 11), (7, 9, 2)],)
Expected Output: (17, [2, 4])
Explanation: Job 2 followed by job 4 gives 6 + 11 = 17, which beats all other compatible subsets.
Input: ([(1, 2, 1000000000), (2, 4, 1000000000), (1, 4, 1999999999), (4, 5, 1)],)
Expected Output: (2000000001, [0, 1, 3])
Explanation: This checks large rewards: jobs 0, 1, and 3 chain together for 2,000,000,001, which is better than taking job 2 plus job 3.
Solution
def solution(jobs):
from bisect import bisect_right
n = len(jobs)
if n == 0:
return (0, [])
ordered = [(s, e, w, i) for i, (s, e, w) in enumerate(jobs)]
ordered.sort(key=lambda job: (job[1], job[0], job[3]))
ends = [job[1] for job in ordered]
dp = [0] * (n + 1)
prev = [0] * (n + 1)
take = [False] * (n + 1)
for i in range(1, n + 1):
s, e, w, idx = ordered[i - 1]
p = bisect_right(ends, s, 0, i - 1)
prev[i] = p
gain_if_take = w + dp[p]
gain_if_skip = dp[i - 1]
if gain_if_take > gain_if_skip:
dp[i] = gain_if_take
take[i] = True
else:
dp[i] = gain_if_skip
chosen = []
i = n
while i > 0:
if take[i]:
chosen.append(ordered[i - 1][3])
i = prev[i]
else:
i -= 1
chosen.reverse()
return (dp[n], chosen)Time complexity: O(N log N). Space complexity: O(N).
Hints
- After sorting by end time, let dp[i] be the best reward using the first i sorted jobs. For the i-th job, binary search the last job whose end time is <= its start time.
- Store both the compatible predecessor position and whether job i was taken. That lets you backtrack one optimal set of original indices. A segment tree with coordinate compression is an alternative O(N log N) approach; an online variant would need an ordered map or balanced tree keyed by end time.