|
| 1 | +--- |
| 2 | +seoTitle: Activity Selection Problem – Greedy Interval Scheduling Algorithm Guide |
| 3 | +description: "Master the Activity Selection Problem using greedy interval scheduling. Covers earliest finish time strategy, exchange argument proof, weighted variant with DP, and full implementations in Python, C++, JavaScript, and Java." |
| 4 | +keywords: "activity selection problem, interval scheduling, greedy algorithm, earliest finish time, non-overlapping intervals, weighted job scheduling, DP, Python, C++, Java, JavaScript, DSA" |
| 5 | +displayTitle: Activity Selection Problem |
| 6 | +--- |
| 7 | + |
| 8 | +> [!info] What is the Activity Selection Problem? |
| 9 | +> The **Activity Selection Problem** asks: given $N$ activities with start times $s_i$ and finish times $f_i$, select the **maximum number of non-overlapping activities** a single resource can perform. |
| 10 | +> The [[Greedy Algorithm Concepts|greedy]] strategy — **always pick the activity that finishes earliest** — is provably optimal and runs in **$O(N \log N)$** time (dominated by sorting). |
| 11 | +> This is the canonical problem for proving greedy correctness using the **exchange argument**. |
| 12 | +
|
| 13 | +- # Explanation |
| 14 | + collapsed:: true |
| 15 | + - Two activities $i$ and $j$ are **compatible** if they do not overlap: $f_i \leq s_j$ or $f_j \leq s_i$. |
| 16 | + - The goal is to find the **largest compatible subset** of activities. |
| 17 | + - |
| 18 | + - ## Why Earliest Finish Time is Greedy-Optimal |
| 19 | + collapsed:: true |
| 20 | + - **Intuition**: Finishing early leaves maximum time for future activities. |
| 21 | + - **Exchange Argument Proof**: |
| 22 | + 1. Let OPT be any optimal solution. Let $a_1$ be the first activity picked by OPT. |
| 23 | + 2. Let $g_1$ be the first activity picked by greedy (earliest finish). |
| 24 | + 3. Since greedy picks earliest finish: $f(g_1) \leq f(a_1)$. |
| 25 | + 4. Replace $a_1$ with $g_1$ in OPT → result is still valid (same or more time freed up) and still optimal size. |
| 26 | + 5. Repeat → greedy solution matches OPT in size. ✅ |
| 27 | + - |
| 28 | + - ## Activity Selection vs Interval Scheduling Variants |
| 29 | + collapsed:: true |
| 30 | + - | Variant | Goal | Strategy | |
| 31 | + |---------|------|----------| |
| 32 | + | **Max Activities (unweighted)** | Max number of non-overlapping activities | Greedy: sort by finish time | |
| 33 | + | **Interval Coloring** | Min resources (rooms/machines) | Greedy: sort by start time, assign to available resource | |
| 34 | + | **Weighted Job Scheduling** | Max total weight of non-overlapping jobs | [[Dynamic Programming Concepts]]: DP + binary search | |
| 35 | + | **Min Interval Coverage** | Fewest intervals to cover a range | Greedy: always pick interval extending coverage farthest | |
| 36 | + - |
| 37 | + - ## Core Properties |
| 38 | + collapsed:: true |
| 39 | + - **Stability**: Not applicable. |
| 40 | + - **Greedy Choice Property**: Selecting the activity with the earliest finish time never eliminates a globally optimal set. |
| 41 | + - **Optimal Substructure**: After selecting activity $k$, the remaining problem is the same — find max activities compatible with $k$ from the remaining set. |
| 42 | + |
| 43 | +- # How It Works |
| 44 | + collapsed:: true |
| 45 | + - ## The Core Idea |
| 46 | + collapsed:: true |
| 47 | + - 1. **Sort** all activities by finish time. |
| 48 | + - 2. Always **select the first compatible activity** (the one that starts after the last selected activity finishes). |
| 49 | + - 3. Repeat until no more activities remain. |
| 50 | + - |
| 51 | + - ```mermaid |
| 52 | + flowchart TD |
| 53 | + A["Sort activities by finish time f_i"] --> B["Select first activity\n(always included in optimal)"] |
| 54 | + B --> C["last_finish = f[0]"] |
| 55 | + C --> D["For each remaining activity i (sorted by finish)"] |
| 56 | + D --> E{"s[i] >= last_finish?\n(compatible with last selected)"} |
| 57 | + E -- Yes --> F["Select activity i\nlast_finish = f[i]"] |
| 58 | + E -- No --> G["Skip activity i\n(overlaps with last selected)"] |
| 59 | + F --> D |
| 60 | + G --> D |
| 61 | + D --> H["✅ Selected set = maximum non-overlapping activities"] |
| 62 | + |
| 63 | + classDef default fill:#1f2937,stroke:#3b82f6,stroke-width:2px,color:#fff; |
| 64 | + ``` |
| 65 | + - |
| 66 | + - ## Step-by-Step Trace |
| 67 | + collapsed:: true |
| 68 | + - ``` |
| 69 | + Activities (start, finish): |
| 70 | + A1=(1,4), A2=(3,5), A3=(0,6), A4=(5,7), A5=(3,9), A6=(5,9), A7=(6,10), A8=(8,11), A9=(8,12), A10=(2,14) |
| 71 | + |
| 72 | + After sorting by finish time: |
| 73 | + A1=(1,4), A2=(3,5), A3=(0,6), A4=(5,7), A5=(3,9), A6=(5,9), A7=(6,10), A8=(8,11), A9=(8,12), A10=(2,14) |
| 74 | + |
| 75 | + Step 1: Select A1 (finish=4). last_finish=4 |
| 76 | + Step 2: A2 start=3 < 4 → skip |
| 77 | + Step 3: A3 start=0 < 4 → skip |
| 78 | + Step 4: A4 start=5 >= 4 → SELECT A4. last_finish=7 |
| 79 | + Step 5: A5 start=3 < 7 → skip |
| 80 | + Step 6: A6 start=5 < 7 → skip |
| 81 | + Step 7: A7 start=6 < 7 → skip |
| 82 | + Step 8: A8 start=8 >= 7 → SELECT A8. last_finish=11 |
| 83 | + Step 9: A9 start=8 < 11 → skip |
| 84 | + Step 10: A10 start=2 < 11 → skip |
| 85 | + |
| 86 | + Selected: {A1, A4, A8} → 3 activities ✅ |
| 87 | + ``` |
| 88 | + |
| 89 | +- # Complexity Analysis |
| 90 | + collapsed:: true |
| 91 | + - | Scenario | Time Complexity | Space Complexity | Notes | |
| 92 | + |---|---|---|---| |
| 93 | + | **Unweighted (Greedy)** | **O(N log N)** | **O(N)** | Dominated by sort; O(1) extra after | |
| 94 | + | **Pre-sorted input** | **O(N)** | **O(1)** | Single pass after sort | |
| 95 | + | **Weighted (DP + BSearch)** | **O(N log N)** | **O(N)** | DP table + binary search for prev compatible | |
| 96 | + - |
| 97 | + - ## Why O(N log N)? |
| 98 | + collapsed:: true |
| 99 | + - Sorting takes $O(N \log N)$. The greedy sweep is a single linear pass $O(N)$. Total = $O(N \log N)$. |
| 100 | + - If activities are already sorted by finish time, the entire algorithm runs in $O(N)$. |
| 101 | + |
| 102 | +- # Implementation |
| 103 | + collapsed:: true |
| 104 | + - > [!note] Activity Selection — Greedy (Unweighted) + Weighted DP Variant |
| 105 | + > The standard greedy returns the count and selected activities. The weighted variant uses DP + binary search to maximize total profit. |
| 106 | + - Languages: [[Python]] · [[Cpp]] · [[Java Script]] · [[Java]] |
| 107 | + - |
| 108 | + - :::code-tabs |
| 109 | + |
| 110 | + ```python |
| 111 | + def activity_selection(activities: list[tuple[int, int]]) -> list[tuple[int, int]]: |
| 112 | + """ |
| 113 | + Greedy Activity Selection — unweighted. |
| 114 | + activities: list of (start, finish) tuples. |
| 115 | + Returns list of selected (start, finish) pairs. |
| 116 | + """ |
| 117 | + # Sort by finish time |
| 118 | + sorted_acts = sorted(activities, key=lambda x: x[1]) |
| 119 | + selected = [sorted_acts[0]] |
| 120 | + last_finish = sorted_acts[0][1] |
| 121 | + |
| 122 | + for start, finish in sorted_acts[1:]: |
| 123 | + if start >= last_finish: |
| 124 | + selected.append((start, finish)) |
| 125 | + last_finish = finish |
| 126 | + |
| 127 | + return selected |
| 128 | + |
| 129 | + # Weighted Job Scheduling — O(N log N) DP |
| 130 | + from bisect import bisect_right |
| 131 | + |
| 132 | + def weighted_job_scheduling(jobs: list[tuple[int, int, int]]) -> int: |
| 133 | + """ |
| 134 | + jobs: list of (start, finish, profit). |
| 135 | + Returns maximum total profit of non-overlapping jobs. |
| 136 | + """ |
| 137 | + jobs.sort(key=lambda x: x[1]) # sort by finish time |
| 138 | + n = len(jobs) |
| 139 | + finish_times = [j[1] for j in jobs] |
| 140 | + dp = [0] * (n + 1) |
| 141 | + |
| 142 | + for i in range(1, n + 1): |
| 143 | + start, finish, profit = jobs[i - 1] |
| 144 | + # Find last job that doesn't conflict (finish <= start of current) |
| 145 | + j = bisect_right(finish_times, start, 0, i - 1) |
| 146 | + # Either include job i (profit + dp[j]) or exclude it (dp[i-1]) |
| 147 | + dp[i] = max(dp[i - 1], profit + dp[j]) |
| 148 | + |
| 149 | + return dp[n] |
| 150 | + |
| 151 | + # Examples |
| 152 | + activities = [(1,4),(3,5),(0,6),(5,7),(3,9),(5,9),(6,10),(8,11),(8,12),(2,14)] |
| 153 | + selected = activity_selection(activities) |
| 154 | + print(f"Selected: {selected}") # [(1,4), (5,7), (8,11)] |
| 155 | + print(f"Count: {len(selected)}") # 3 |
| 156 | + |
| 157 | + jobs = [(1, 4, 20), (3, 5, 30), (0, 6, 15), (5, 7, 40)] |
| 158 | + print(f"Max Profit: {weighted_job_scheduling(jobs)}") # 60 (job1 + job4) |
| 159 | + ``` |
| 160 | + |
| 161 | + ```c++ |
| 162 | + #include <iostream> |
| 163 | + #include <vector> |
| 164 | + #include <algorithm> |
| 165 | + |
| 166 | + using Activity = std::pair<int, int>; // (start, finish) |
| 167 | + |
| 168 | + std::vector<Activity> activitySelection(std::vector<Activity> activities) { |
| 169 | + std::sort(activities.begin(), activities.end(), |
| 170 | + [](const Activity& a, const Activity& b) { return a.second < b.second; }); |
| 171 | + |
| 172 | + std::vector<Activity> selected = {activities[0]}; |
| 173 | + int lastFinish = activities[0].second; |
| 174 | + |
| 175 | + for (size_t i = 1; i < activities.size(); ++i) { |
| 176 | + if (activities[i].first >= lastFinish) { |
| 177 | + selected.push_back(activities[i]); |
| 178 | + lastFinish = activities[i].second; |
| 179 | + } |
| 180 | + } |
| 181 | + return selected; |
| 182 | + } |
| 183 | + |
| 184 | + int main() { |
| 185 | + std::vector<Activity> acts = {{1,4},{3,5},{0,6},{5,7},{3,9},{5,9},{6,10},{8,11}}; |
| 186 | + auto result = activitySelection(acts); |
| 187 | + std::cout << "Selected " << result.size() << " activities:\n"; |
| 188 | + for (auto [s, f] : result) |
| 189 | + std::cout << " (" << s << ", " << f << ")\n"; |
| 190 | + return 0; |
| 191 | + } |
| 192 | + ``` |
| 193 | + |
| 194 | + ```javascript |
| 195 | + function activitySelection(activities) { |
| 196 | + // activities: [{start, finish}, ...] |
| 197 | + activities.sort((a, b) => a.finish - b.finish); |
| 198 | + const selected = [activities[0]]; |
| 199 | + let lastFinish = activities[0].finish; |
| 200 | + |
| 201 | + for (let i = 1; i < activities.length; i++) { |
| 202 | + if (activities[i].start >= lastFinish) { |
| 203 | + selected.push(activities[i]); |
| 204 | + lastFinish = activities[i].finish; |
| 205 | + } |
| 206 | + } |
| 207 | + return selected; |
| 208 | + } |
| 209 | + |
| 210 | + const acts = [ |
| 211 | + {start:1,finish:4},{start:3,finish:5},{start:0,finish:6}, |
| 212 | + {start:5,finish:7},{start:8,finish:11} |
| 213 | + ]; |
| 214 | + const result = activitySelection(acts); |
| 215 | + console.log("Count:", result.length); // 3 |
| 216 | + console.log("Selected:", result); |
| 217 | + ``` |
| 218 | + |
| 219 | + ```java |
| 220 | + import java.util.*; |
| 221 | + |
| 222 | + public class ActivitySelection { |
| 223 | + public static List<int[]> select(int[][] activities) { |
| 224 | + // activities[i] = {start, finish} |
| 225 | + Arrays.sort(activities, (a, b) -> a[1] - b[1]); |
| 226 | + List<int[]> selected = new ArrayList<>(); |
| 227 | + selected.add(activities[0]); |
| 228 | + int lastFinish = activities[0][1]; |
| 229 | + |
| 230 | + for (int i = 1; i < activities.length; i++) { |
| 231 | + if (activities[i][0] >= lastFinish) { |
| 232 | + selected.add(activities[i]); |
| 233 | + lastFinish = activities[i][1]; |
| 234 | + } |
| 235 | + } |
| 236 | + return selected; |
| 237 | + } |
| 238 | + |
| 239 | + public static void main(String[] args) { |
| 240 | + int[][] acts = {{1,4},{3,5},{0,6},{5,7},{3,9},{8,11}}; |
| 241 | + List<int[]> result = select(acts); |
| 242 | + System.out.println("Count: " + result.size()); |
| 243 | + for (int[] a : result) |
| 244 | + System.out.println(" (" + a[0] + ", " + a[1] + ")"); |
| 245 | + } |
| 246 | + } |
| 247 | + ``` |
| 248 | + |
| 249 | + ::: |
| 250 | + |
| 251 | +- # Alternative Variant (Interval Coloring — Minimum Machines) |
| 252 | + collapsed:: true |
| 253 | + - > [!tip] Interval Coloring — Find Minimum Number of Machines Needed |
| 254 | + > Given $N$ activities, what is the minimum number of machines (rooms, processors) needed so all activities run simultaneously? Greedy: **sort by start time**, maintain a min-heap of finish times. If a machine is free (heap top ≤ current start), reuse it; otherwise add a new machine. |
| 255 | + - |
| 256 | + - :::code-tabs |
| 257 | + |
| 258 | + ```python |
| 259 | + import heapq |
| 260 | + |
| 261 | + def min_machines(activities: list[tuple[int, int]]) -> int: |
| 262 | + """ |
| 263 | + Minimum machines to run all activities without conflicts. |
| 264 | + Greedy: sort by start, use min-heap of finish times. |
| 265 | + """ |
| 266 | + if not activities: |
| 267 | + return 0 |
| 268 | + activities.sort(key=lambda x: x[0]) # sort by start time |
| 269 | + heap = [] # min-heap of finish times (machines in use) |
| 270 | + |
| 271 | + for start, finish in activities: |
| 272 | + if heap and heap[0] <= start: |
| 273 | + heapq.heapreplace(heap, finish) # reuse freed machine |
| 274 | + else: |
| 275 | + heapq.heappush(heap, finish) # add new machine |
| 276 | + |
| 277 | + return len(heap) |
| 278 | + |
| 279 | + # Example |
| 280 | + activities = [(0,6),(1,4),(3,5),(5,7),(3,9),(5,9),(6,10),(8,11)] |
| 281 | + print(f"Min machines: {min_machines(activities)}") # 3 |
| 282 | + ``` |
| 283 | + |
| 284 | + ::: |
| 285 | + |
| 286 | +- # When to Use Activity Selection |
| 287 | + collapsed:: true |
| 288 | + - ```mermaid |
| 289 | + flowchart TD |
| 290 | + Q{"Do you have intervals\nwith start/finish times?"} |
| 291 | + Q -- No --> R1["Use standard sorting\nor scheduling algorithms"] |
| 292 | + Q -- Yes --> S1{"Are all activities\nequally valuable (unweighted)?"} |
| 293 | + S1 -- Yes --> R2["✅ Greedy: Sort by finish time\nO(N log N), maximum count"] |
| 294 | + S1 -- No --> S2{"Do activities have\ndifferent profits/weights?"} |
| 295 | + S2 -- Yes --> R3["✅ Weighted Job Scheduling\nDP + Binary Search, O(N log N)"] |
| 296 | + S2 -- No --> R4["✅ Greedy: earliest finish first"] |
| 297 | + |
| 298 | + classDef default fill:#1f2937,stroke:#3b82f6,stroke-width:2px,color:#fff; |
| 299 | + ``` |
| 300 | + - |
| 301 | + - ## ✅ Use Activity Selection When |
| 302 | + - Maximizing the **count** of non-overlapping intervals (all equal value). |
| 303 | + - **Room/machine allocation** problems — minimize resources used. |
| 304 | + - **Meeting scheduling** — fit the most meetings in a day. |
| 305 | + - LeetCode-style problems: "Non-overlapping Intervals", "Meeting Rooms II". |
| 306 | + - |
| 307 | + - ## ❌ Avoid Simple Greedy When |
| 308 | + - Activities have **different profits/weights** — switch to Weighted Job Scheduling DP. |
| 309 | + - The constraint is not just non-overlap but involves **dependencies** between activities. |
| 310 | + |
| 311 | +- # Key Takeaways |
| 312 | + collapsed:: true |
| 313 | + - **Earliest Finish First** — The greedy choice is always picking the activity with the earliest finish time, maximizing remaining time for future activities. |
| 314 | + - **Exchange Argument Proven** — Correctness is guaranteed by the exchange argument: any optimal solution can be transformed into the greedy solution without reducing the count. |
| 315 | + - **O(N log N) → O(N)** — Sorting dominates; after sorting, a single linear scan is enough. |
| 316 | + - **Weighted Variant Needs DP** — When activities have different profits, greedy fails. Use DP + binary search ([[Dynamic Programming Concepts]]) for the weighted version. |
| 317 | + - **Interval Coloring Dual** — The minimum number of machines = the maximum overlap depth at any point (use min-heap of finish times). |
| 318 | + - **LeetCode Applications** — "Non-overlapping Intervals" (#435), "Meeting Rooms II" (#253), "Minimum Number of Arrows to Burst Balloons" (#452). |
| 319 | + |
| 320 | +- # More Learn |
| 321 | + collapsed:: true |
| 322 | + - ## GitHub & Webs |
| 323 | + - [GeeksforGeeks → Activity Selection Problem](https://www.geeksforgeeks.org/activity-selection-problem-greedy-algo-1/) |
| 324 | + - [CP Algorithms → Scheduling](https://cp-algorithms.com/greedy/job_scheduling.html) |
| 325 | + - [LeetCode → Non-overlapping Intervals (Problem 435)](https://leetcode.com/problems/non-overlapping-intervals/) |
| 326 | + - [LeetCode → Meeting Rooms II (Problem 253)](https://leetcode.com/problems/meeting-rooms-ii/) |
0 commit comments