|
14 | 14 |
|
15 | 15 | Time Complexity: O(n log n) |
16 | 16 | Space Complexity: O(n) |
| 17 | +
|
| 18 | +Reference: https://en.wikipedia.org/wiki/Interval_scheduling#Weighted_interval_scheduling |
17 | 19 | """ |
18 | 20 |
|
19 | 21 |
|
20 | | -def find_last_non_conflicting(jobs, index): |
| 22 | +def find_last_non_conflicting_job( |
| 23 | + jobs: list[tuple[int, int, int]], current_job_index: int |
| 24 | +) -> int: |
21 | 25 | """ |
22 | | - Binary search to find the last job that doesn't overlap |
23 | | - with the current job (at index). |
| 26 | + Binary search to find the last job that doesn't overlap with the current job. |
| 27 | +
|
| 28 | + Args: |
| 29 | + jobs: List of jobs sorted by finish time, each job is |
| 30 | + (start_time, end_time, profit) |
| 31 | + current_job_index: Index of the current job for which we need to find |
| 32 | + non-conflicting jobs Returns: |
| 33 | + Index of the last non-conflicting job, or -1 if no such job exists |
| 34 | +
|
| 35 | + Examples: |
| 36 | + >>> jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40)] |
| 37 | + >>> find_last_non_conflicting_job(jobs, 2) |
| 38 | + 0 |
| 39 | + >>> find_last_non_conflicting_job(jobs, 1) |
| 40 | + -1 |
24 | 41 | """ |
25 | | - low, high = 0, index - 1 |
| 42 | + low, high = 0, current_job_index - 1 |
| 43 | + last_non_conflicting_index = -1 |
| 44 | + |
26 | 45 | while low <= high: |
27 | 46 | mid = (low + high) // 2 |
28 | | - if jobs[mid][1] <= jobs[index][0]: |
29 | | - if mid + 1 < index and jobs[mid + 1][1] <= jobs[index][0]: |
30 | | - low = mid + 1 |
31 | | - else: |
32 | | - return mid |
| 47 | + if jobs[mid][1] <= jobs[current_job_index][0]: |
| 48 | + last_non_conflicting_index = mid |
| 49 | + low = mid + 1 |
33 | 50 | else: |
34 | 51 | high = mid - 1 |
35 | | - return -1 |
| 52 | + |
| 53 | + return last_non_conflicting_index |
36 | 54 |
|
37 | 55 |
|
38 | | -def weighted_job_scheduling(jobs): |
| 56 | +def weighted_job_scheduling_with_maximum_profit( |
| 57 | + jobs: list[tuple[int, int, int]], |
| 58 | +) -> int: |
39 | 59 | """ |
40 | | - Function to find the maximum profit from a set of jobs. |
| 60 | + Find the maximum profit from a set of jobs without overlapping intervals. |
| 61 | +
|
| 62 | + Args: |
| 63 | + jobs: List of tuples where each tuple represents (start_time, end_time, profit) |
41 | 64 |
|
42 | | - Parameters: |
43 | | - jobs (list of tuples): Each tuple represents (start_time, end_time, profit) |
44 | 65 | Returns: |
45 | | - int: Maximum profit achievable without overlapping jobs. |
| 66 | + Maximum profit achievable without overlapping jobs |
| 67 | +
|
| 68 | + Raises: |
| 69 | + ValueError: If jobs list is empty or contains invalid job data |
| 70 | +
|
| 71 | + Examples: |
| 72 | + >>> jobs1 = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] |
| 73 | + >>> weighted_job_scheduling_with_maximum_profit(jobs1) |
| 74 | + 120 |
| 75 | + >>> jobs2 = [(1, 2, 10), (2, 3, 20), (3, 4, 30)] |
| 76 | + >>> weighted_job_scheduling_with_maximum_profit(jobs2) |
| 77 | + 60 |
| 78 | + >>> weighted_job_scheduling_with_maximum_profit([(1, 4, 100), (2, 3, 50)]) |
| 79 | + 100 |
| 80 | + >>> weighted_job_scheduling_with_maximum_profit([]) |
| 81 | + 0 |
| 82 | + >>> weighted_job_scheduling_with_maximum_profit([(1, 1, 10)]) |
| 83 | + Traceback (most recent call last): |
| 84 | + ... |
| 85 | + ValueError: Invalid job: start time must be less than end time |
46 | 86 | """ |
| 87 | + if not jobs: |
| 88 | + return 0 |
| 89 | + |
| 90 | + # Validate job data |
| 91 | + for start_time, end_time, profit in jobs: |
| 92 | + if ( |
| 93 | + not isinstance(start_time, int) |
| 94 | + or not isinstance(end_time, int) |
| 95 | + or not isinstance(profit, int) |
| 96 | + ): |
| 97 | + raise ValueError("Job times and profit must be integers") |
| 98 | + if start_time >= end_time: |
| 99 | + raise ValueError("Invalid job: start time must be less than end time") |
| 100 | + if profit < 0: |
| 101 | + raise ValueError("Job profit cannot be negative") |
| 102 | + |
| 103 | + # Sort jobs by their finish time |
| 104 | + sorted_jobs_by_finish_time = sorted(jobs, key=lambda job: job[1]) |
| 105 | + number_of_jobs = len(sorted_jobs_by_finish_time) |
| 106 | + |
| 107 | + # Dynamic programming array to store maximum profit up to each job |
| 108 | + maximum_profit_up_to_job = [0] * number_of_jobs |
| 109 | + maximum_profit_up_to_job[0] = sorted_jobs_by_finish_time[0][2] |
| 110 | + |
| 111 | + # Fill the DP array |
| 112 | + for current_job_index in range(1, number_of_jobs): |
| 113 | + # Profit including current job |
| 114 | + current_job_profit = sorted_jobs_by_finish_time[current_job_index][2] |
47 | 115 |
|
48 | | - # Step 1: Sort jobs by their finish time |
49 | | - jobs.sort(key=lambda x: x[1]) |
| 116 | + # Find the last non-conflicting job |
| 117 | + last_non_conflicting_index = find_last_non_conflicting_job( |
| 118 | + sorted_jobs_by_finish_time, current_job_index |
| 119 | + ) |
50 | 120 |
|
51 | | - n = len(jobs) |
52 | | - dp = [0] * n # dp[i] will store the max profit up to job i |
53 | | - dp[0] = jobs[0][2] # First job profit is the base case |
| 121 | + profit_including_current_job = current_job_profit |
| 122 | + if last_non_conflicting_index != -1: |
| 123 | + profit_including_current_job += maximum_profit_up_to_job[ |
| 124 | + last_non_conflicting_index |
| 125 | + ] |
54 | 126 |
|
55 | | - # Step 2: Iterate through all jobs |
56 | | - for i in range(1, n): |
57 | | - # Include current job |
58 | | - include_profit = jobs[i][2] |
| 127 | + # Maximum profit is either including current job or excluding it |
| 128 | + maximum_profit_up_to_job[current_job_index] = max( |
| 129 | + profit_including_current_job, |
| 130 | + maximum_profit_up_to_job[current_job_index - 1], |
| 131 | + ) |
59 | 132 |
|
60 | | - # Find the last non-conflicting job |
61 | | - j = find_last_non_conflicting(jobs, i) |
62 | | - if j != -1: |
63 | | - include_profit += dp[j] |
| 133 | + return maximum_profit_up_to_job[number_of_jobs - 1] |
| 134 | + |
| 135 | + |
| 136 | +def demonstrate_weighted_job_scheduling_algorithm() -> None: |
| 137 | + """ |
| 138 | + Demonstrate the weighted job scheduling algorithm with example test cases. |
| 139 | +
|
| 140 | + Examples: |
| 141 | + >>> demonstrate_weighted_job_scheduling_algorithm() # doctest: +ELLIPSIS |
| 142 | + Weighted Job Scheduling Algorithm Demonstration |
| 143 | + ... |
| 144 | + Maximum Profit: 120 |
| 145 | + """ |
| 146 | + print("Weighted Job Scheduling Algorithm Demonstration") |
| 147 | + print("=" * 50) |
64 | 148 |
|
65 | | - # Exclude current job (take previous best) |
66 | | - dp[i] = max(include_profit, dp[i - 1]) |
| 149 | + # Example jobs: (start_time, end_time, profit) |
| 150 | + example_jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] |
67 | 151 |
|
68 | | - # Step 3: Return the maximum profit at the end |
69 | | - return dp[-1] |
| 152 | + print(f"Input Jobs: {example_jobs}") |
| 153 | + maximum_profit = weighted_job_scheduling_with_maximum_profit(example_jobs) |
| 154 | + print(f"Maximum Profit: {maximum_profit}") |
70 | 155 |
|
71 | 156 |
|
72 | | -# Example usage / Test case |
73 | 157 | if __name__ == "__main__": |
74 | | - # Each job is represented as (start_time, end_time, profit) |
75 | | - jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] |
| 158 | + import doctest |
76 | 159 |
|
77 | | - print("Maximum Profit:", weighted_job_scheduling(jobs)) |
| 160 | + doctest.testmod() |
| 161 | + demonstrate_weighted_job_scheduling_algorithm() |
0 commit comments