From 7bc79fa3a6b05e8a0ce6c7da278b4723aeb4d17c Mon Sep 17 00:00:00 2001 From: Prince <167101466+pkprajapati7402@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:07:36 +0000 Subject: [PATCH 1/2] feat: Add Weighted Job Scheduling algorithm to dynamic programming - Implemented efficient Weighted Job Scheduling algorithm using Dynamic Programming and Binary Search - Time Complexity: O(n log n), Space Complexity: O(n) - Added comprehensive documentation and example usage - Includes proper author attribution and date - Fixed linting issues: removed unused imports and applied formatting Hacktoberfest contribution - Dynamic Programming implementation for finding maximum profit from non-overlapping job intervals --- DIRECTORY.md | 3 + .../weighted_job_scheduling.py | 77 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 dynamic_programming/weighted_job_scheduling.py diff --git a/DIRECTORY.md b/DIRECTORY.md index 36acb3b97f1e..2c883e7106ad 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -26,6 +26,7 @@ * [Word Break](backtracking/word_break.py) * [Word Ladder](backtracking/word_ladder.py) * [Word Search](backtracking/word_search.py) + * [Weighted Job Scheduling](backtracking/weighted_job_scheduling.py) ## Bit Manipulation * [Binary And Operator](bit_manipulation/binary_and_operator.py) @@ -880,6 +881,8 @@ * [Sdes](other/sdes.py) * [Tower Of Hanoi](other/tower_of_hanoi.py) * [Word Search](other/word_search.py) + * [Weighted Job Scheduling](backtracking/weighted_job_scheduling.py) + ## Physics * [Altitude Pressure](physics/altitude_pressure.py) diff --git a/dynamic_programming/weighted_job_scheduling.py b/dynamic_programming/weighted_job_scheduling.py new file mode 100644 index 000000000000..9c3acc31aeb2 --- /dev/null +++ b/dynamic_programming/weighted_job_scheduling.py @@ -0,0 +1,77 @@ +""" +Author : Prince Kumar Prajapati +Date : October 9, 2025 + +Weighted Job Scheduling Problem +-------------------------------- +Given N jobs where every job has a start time, finish time, and profit, +find the maximum profit subset of jobs such that no two jobs overlap. + +Approach: +- Sort all jobs by their finish time. +- For each job, find the last non-conflicting job using binary search. +- Use Dynamic Programming to build up the maximum profit table. + +Time Complexity: O(n log n) +Space Complexity: O(n) +""" + + +def find_last_non_conflicting(jobs, index): + """ + Binary search to find the last job that doesn't overlap + with the current job (at index). + """ + low, high = 0, index - 1 + while low <= high: + mid = (low + high) // 2 + if jobs[mid][1] <= jobs[index][0]: + if mid + 1 < index and jobs[mid + 1][1] <= jobs[index][0]: + low = mid + 1 + else: + return mid + else: + high = mid - 1 + return -1 + + +def weighted_job_scheduling(jobs): + """ + Function to find the maximum profit from a set of jobs. + + Parameters: + jobs (list of tuples): Each tuple represents (start_time, end_time, profit) + Returns: + int: Maximum profit achievable without overlapping jobs. + """ + + # Step 1: Sort jobs by their finish time + jobs.sort(key=lambda x: x[1]) + + n = len(jobs) + dp = [0] * n # dp[i] will store the max profit up to job i + dp[0] = jobs[0][2] # First job profit is the base case + + # Step 2: Iterate through all jobs + for i in range(1, n): + # Include current job + include_profit = jobs[i][2] + + # Find the last non-conflicting job + j = find_last_non_conflicting(jobs, i) + if j != -1: + include_profit += dp[j] + + # Exclude current job (take previous best) + dp[i] = max(include_profit, dp[i - 1]) + + # Step 3: Return the maximum profit at the end + return dp[-1] + + +# Example usage / Test case +if __name__ == "__main__": + # Each job is represented as (start_time, end_time, profit) + jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] + + print("Maximum Profit:", weighted_job_scheduling(jobs)) From f8d9a0070ddd83bd942b29f20fee44e0af622424 Mon Sep 17 00:00:00 2001 From: Prince <167101466+pkprajapati7402@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:19:10 +0000 Subject: [PATCH 2/2] fix: Format code to meet line length requirements - Fixed remaining line length issues in docstrings - All lines now comply with 88 character limit - Removed trailing whitespace - Addresses algorithms-keeper bot formatting requirements --- .../weighted_job_scheduling.py | 156 ++++++++++++++---- 1 file changed, 120 insertions(+), 36 deletions(-) diff --git a/dynamic_programming/weighted_job_scheduling.py b/dynamic_programming/weighted_job_scheduling.py index 9c3acc31aeb2..aad7131d8753 100644 --- a/dynamic_programming/weighted_job_scheduling.py +++ b/dynamic_programming/weighted_job_scheduling.py @@ -14,64 +14,148 @@ Time Complexity: O(n log n) Space Complexity: O(n) + +Reference: https://en.wikipedia.org/wiki/Interval_scheduling#Weighted_interval_scheduling """ -def find_last_non_conflicting(jobs, index): +def find_last_non_conflicting_job( + jobs: list[tuple[int, int, int]], current_job_index: int +) -> int: """ - Binary search to find the last job that doesn't overlap - with the current job (at index). + Binary search to find the last job that doesn't overlap with the current job. + + Args: + jobs: List of jobs sorted by finish time, each job is + (start_time, end_time, profit) + current_job_index: Index of the current job for which we need to find + non-conflicting jobs Returns: + Index of the last non-conflicting job, or -1 if no such job exists + + Examples: + >>> jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40)] + >>> find_last_non_conflicting_job(jobs, 2) + 0 + >>> find_last_non_conflicting_job(jobs, 1) + -1 """ - low, high = 0, index - 1 + low, high = 0, current_job_index - 1 + last_non_conflicting_index = -1 + while low <= high: mid = (low + high) // 2 - if jobs[mid][1] <= jobs[index][0]: - if mid + 1 < index and jobs[mid + 1][1] <= jobs[index][0]: - low = mid + 1 - else: - return mid + if jobs[mid][1] <= jobs[current_job_index][0]: + last_non_conflicting_index = mid + low = mid + 1 else: high = mid - 1 - return -1 + + return last_non_conflicting_index -def weighted_job_scheduling(jobs): +def weighted_job_scheduling_with_maximum_profit( + jobs: list[tuple[int, int, int]], +) -> int: """ - Function to find the maximum profit from a set of jobs. + Find the maximum profit from a set of jobs without overlapping intervals. + + Args: + jobs: List of tuples where each tuple represents (start_time, end_time, profit) - Parameters: - jobs (list of tuples): Each tuple represents (start_time, end_time, profit) Returns: - int: Maximum profit achievable without overlapping jobs. + Maximum profit achievable without overlapping jobs + + Raises: + ValueError: If jobs list is empty or contains invalid job data + + Examples: + >>> jobs1 = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] + >>> weighted_job_scheduling_with_maximum_profit(jobs1) + 120 + >>> jobs2 = [(1, 2, 10), (2, 3, 20), (3, 4, 30)] + >>> weighted_job_scheduling_with_maximum_profit(jobs2) + 60 + >>> weighted_job_scheduling_with_maximum_profit([(1, 4, 100), (2, 3, 50)]) + 100 + >>> weighted_job_scheduling_with_maximum_profit([]) + 0 + >>> weighted_job_scheduling_with_maximum_profit([(1, 1, 10)]) + Traceback (most recent call last): + ... + ValueError: Invalid job: start time must be less than end time """ + if not jobs: + return 0 + + # Validate job data + for start_time, end_time, profit in jobs: + if ( + not isinstance(start_time, int) + or not isinstance(end_time, int) + or not isinstance(profit, int) + ): + raise ValueError("Job times and profit must be integers") + if start_time >= end_time: + raise ValueError("Invalid job: start time must be less than end time") + if profit < 0: + raise ValueError("Job profit cannot be negative") + + # Sort jobs by their finish time + sorted_jobs_by_finish_time = sorted(jobs, key=lambda job: job[1]) + number_of_jobs = len(sorted_jobs_by_finish_time) + + # Dynamic programming array to store maximum profit up to each job + maximum_profit_up_to_job = [0] * number_of_jobs + maximum_profit_up_to_job[0] = sorted_jobs_by_finish_time[0][2] + + # Fill the DP array + for current_job_index in range(1, number_of_jobs): + # Profit including current job + current_job_profit = sorted_jobs_by_finish_time[current_job_index][2] - # Step 1: Sort jobs by their finish time - jobs.sort(key=lambda x: x[1]) + # Find the last non-conflicting job + last_non_conflicting_index = find_last_non_conflicting_job( + sorted_jobs_by_finish_time, current_job_index + ) - n = len(jobs) - dp = [0] * n # dp[i] will store the max profit up to job i - dp[0] = jobs[0][2] # First job profit is the base case + profit_including_current_job = current_job_profit + if last_non_conflicting_index != -1: + profit_including_current_job += maximum_profit_up_to_job[ + last_non_conflicting_index + ] - # Step 2: Iterate through all jobs - for i in range(1, n): - # Include current job - include_profit = jobs[i][2] + # Maximum profit is either including current job or excluding it + maximum_profit_up_to_job[current_job_index] = max( + profit_including_current_job, + maximum_profit_up_to_job[current_job_index - 1], + ) - # Find the last non-conflicting job - j = find_last_non_conflicting(jobs, i) - if j != -1: - include_profit += dp[j] + return maximum_profit_up_to_job[number_of_jobs - 1] + + +def demonstrate_weighted_job_scheduling_algorithm() -> None: + """ + Demonstrate the weighted job scheduling algorithm with example test cases. + + Examples: + >>> demonstrate_weighted_job_scheduling_algorithm() # doctest: +ELLIPSIS + Weighted Job Scheduling Algorithm Demonstration + ... + Maximum Profit: 120 + """ + print("Weighted Job Scheduling Algorithm Demonstration") + print("=" * 50) - # Exclude current job (take previous best) - dp[i] = max(include_profit, dp[i - 1]) + # Example jobs: (start_time, end_time, profit) + example_jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] - # Step 3: Return the maximum profit at the end - return dp[-1] + print(f"Input Jobs: {example_jobs}") + maximum_profit = weighted_job_scheduling_with_maximum_profit(example_jobs) + print(f"Maximum Profit: {maximum_profit}") -# Example usage / Test case if __name__ == "__main__": - # Each job is represented as (start_time, end_time, profit) - jobs = [(1, 3, 50), (2, 4, 10), (3, 5, 40), (3, 6, 70)] + import doctest - print("Maximum Profit:", weighted_job_scheduling(jobs)) + doctest.testmod() + demonstrate_weighted_job_scheduling_algorithm()