Bird
Raised Fist0
DSA Goprogramming~15 mins

Activity Selection Problem in DSA Go - Deep Dive

Choose your learning style10 modes available
Overview - Activity Selection Problem
What is it?
The Activity Selection Problem is about choosing the maximum number of activities that don't overlap in time. Each activity has a start time and an end time. The goal is to select as many activities as possible so that no two activities happen at the same time. This helps in scheduling tasks efficiently.
Why it matters
Without this problem's solution, scheduling would be inefficient, leading to wasted time and resources. For example, if you book overlapping meetings, you can't attend all. Solving this problem helps in planning events, CPU task scheduling, and resource allocation where time conflicts matter.
Where it fits
Before learning this, you should understand arrays and sorting basics. After this, you can explore more complex scheduling problems like interval partitioning or dynamic programming approaches to weighted activity selection.
Mental Model
Core Idea
Pick the earliest finishing activity first to leave room for more activities later.
Think of it like...
Imagine you have a single meeting room and many people want to use it. To fit the most meetings, you always let the meeting that ends soonest go first, so the room becomes free quickly for others.
Activities sorted by finish time:
┌───────────────┐
│ Activity List │
├───────────────┤
│ A1: 1 - 4    │
│ A2: 3 - 5    │
│ A3: 0 - 6    │
│ A4: 5 - 7    │
│ A5: 8 - 9    │
└───────────────┘

Selection process:
Start -> Pick A1 (ends at 4)
Next pick A4 (starts at 5)
Next pick A5 (starts at 8)
Result: A1 -> A4 -> A5
Build-Up - 7 Steps
1
FoundationUnderstanding Activities and Time Slots
🤔
Concept: Introduce what activities are and how start and end times define their duration.
An activity is a task with a start time and an end time. For example, Activity A starts at 1 and ends at 4. If two activities overlap in time, they cannot be done together. We want to find a way to pick activities that do not overlap.
Result
You understand that activities are intervals on a timeline and overlapping means conflicting times.
Understanding activities as time intervals is key to seeing why some can't happen together.
2
FoundationSorting Activities by Finish Time
🤔
Concept: Sorting activities by their end times helps in making the best choices.
Given a list of activities, we sort them so the one that finishes earliest comes first. For example, activities [(1,4), (3,5), (0,6)] become [(1,4), (3,5), (0,6)] sorted by finish times 4,5,6 respectively.
Result
Activities are ordered to consider earliest finishing first.
Sorting by finish time sets the stage for a greedy choice that leads to the optimal solution.
3
IntermediateGreedy Choice: Pick Earliest Finishing Activity
🤔Before reading on: do you think picking the longest activity first or the earliest finishing activity first leads to more total activities? Commit to your answer.
Concept: Choosing the activity that finishes earliest leaves more time for others.
Start by picking the first activity in the sorted list (earliest finish). Then skip all activities that start before this one ends. Repeat picking the next earliest finishing activity that starts after the last selected one ends.
Result
Selected activities maximize the number of non-overlapping tasks.
Knowing that earliest finishing activity frees up time for more activities is the heart of the greedy strategy.
4
IntermediateImplementing the Selection Algorithm in Go
🤔Before reading on: do you think a loop or recursion is better to implement the selection? Commit to your answer.
Concept: Use a simple loop to select activities based on sorted finish times.
Sort activities by finish time. Initialize selected list with first activity. For each next activity, if its start time is >= last selected activity's finish time, select it. Return the list of selected activities. Example Go code: package main import ( "fmt" "sort" ) type Activity struct { start, finish int } func activitySelection(activities []Activity) []Activity { sort.Slice(activities, func(i, j int) bool { return activities[i].finish < activities[j].finish }) selected := []Activity{activities[0]} lastFinish := activities[0].finish for _, act := range activities[1:] { if act.start >= lastFinish { selected = append(selected, act) lastFinish = act.finish } } return selected } func main() { acts := []Activity{{1,4},{3,5},{0,6},{5,7},{8,9},{5,9}} res := activitySelection(acts) for _, a := range res { fmt.Printf("%d -> %d\n", a.start, a.finish) } }
Result
Output: 1 -> 4 5 -> 7 8 -> 9
Implementing the algorithm concretely shows how sorting and selection combine to solve the problem efficiently.
5
IntermediateAnalyzing Time Complexity
🤔Before reading on: do you think sorting or selection dominates the running time? Commit to your answer.
Concept: Sorting takes most time; selection is linear.
Sorting the activities by finish time takes O(n log n) time. Selecting activities by scanning once takes O(n) time. Overall, the algorithm runs in O(n log n) due to sorting.
Result
You understand the algorithm is efficient for large inputs.
Knowing where the time is spent helps optimize and understand scalability.
6
AdvancedHandling Edge Cases and Equal Finish Times
🤔Before reading on: do you think activities with the same finish time can both be selected? Commit to your answer.
Concept: When finish times tie, start times decide selection order.
If two activities finish at the same time, picking the one that starts earlier or later can affect the result. The standard approach is stable sorting by finish time and picking in order. Activities with the same finish time but overlapping start times cannot both be selected. Example: Activities (1,4) and (2,4) finish at 4, only one can be chosen.
Result
Algorithm correctly handles ties and avoids overlaps.
Understanding tie cases prevents subtle bugs in scheduling.
7
ExpertExtending to Weighted Activity Selection Problem
🤔Before reading on: do you think greedy approach works when activities have different values? Commit to your answer.
Concept: When activities have weights (values), greedy fails; dynamic programming is needed.
In weighted activity selection, each activity has a value. Goal: maximize total value, not just count. Greedy by earliest finish may miss better combinations. Dynamic programming finds optimal weighted subset by considering compatible activities and their values. This is a more complex problem requiring memoization or bottom-up DP.
Result
You see limits of greedy and the need for advanced methods.
Knowing when greedy fails guides choosing the right algorithm for complex scheduling.
Under the Hood
The algorithm sorts activities by their finish times, then iterates through them once. It keeps track of the last selected activity's finish time and only selects the next activity if it starts after or exactly when the last one finished. This greedy choice ensures local optimality at each step, which leads to a global optimum for this problem.
Why designed this way?
This problem was formulated to model real-world scheduling where resources are limited. The greedy approach was chosen because it is simple, efficient, and provably optimal for the unweighted case. Alternatives like brute force check all subsets but are too slow. The greedy method balances speed and correctness.
┌───────────────┐
│ Activities    │
│ (start, end)  │
├───────────────┤
│ (1,4)         │
│ (3,5)         │
│ (0,6)         │
│ (5,7)         │
│ (8,9)         │
└───────────────┘
       ↓ Sort by finish time
┌───────────────┐
│ Sorted List   │
│ (1,4), (3,5), │
│ (0,6), (5,7), │
│ (8,9)         │
└───────────────┘
       ↓ Iterate and select
┌───────────────┐
│ Selected      │
│ (1,4)         │
│ (5,7)         │
│ (8,9)         │
└───────────────┘
Myth Busters - 3 Common Misconceptions
Quick: Does picking the longest activity first always maximize the number of activities? Commit yes or no.
Common Belief:Choosing the longest activity first will maximize the total scheduled time and number of activities.
Tap to reveal reality
Reality:Picking the longest activity first often reduces the total number of activities because it blocks more time, leaving less room for others.
Why it matters:Following this wrong belief leads to fewer scheduled activities and inefficient use of time.
Quick: Can greedy approach solve weighted activity selection optimally? Commit yes or no.
Common Belief:The greedy method that picks earliest finishing activities also works when activities have different values.
Tap to reveal reality
Reality:Greedy fails for weighted activities; dynamic programming is required for optimal solutions.
Why it matters:Using greedy for weighted cases leads to suboptimal schedules and lost value.
Quick: If two activities finish at the same time, can both be selected? Commit yes or no.
Common Belief:Activities with the same finish time can both be selected if their start times differ.
Tap to reveal reality
Reality:If activities overlap in time, even if they finish simultaneously, only one can be selected.
Why it matters:Ignoring overlaps causes scheduling conflicts and incorrect results.
Expert Zone
1
The greedy choice property holds only because activities are sorted by finish time, not start time or duration.
2
Stable sorting preserves the original order of activities with equal finish times, which can affect tie-breaking.
3
In practice, pre-processing to remove dominated activities (those fully contained in others) can speed up selection.
When NOT to use
Do not use this greedy approach when activities have weights or values; instead, use dynamic programming. Also, if activities can be preempted or partially scheduled, more complex algorithms are needed.
Production Patterns
Used in CPU job scheduling where tasks are non-preemptive and have fixed start/end times. Also applied in event planning software to maximize room usage and in network bandwidth allocation for non-overlapping transmissions.
Connections
Interval Graph Coloring
Builds-on the idea of non-overlapping intervals but focuses on coloring overlapping intervals with minimum colors.
Understanding activity selection helps grasp interval graph properties and resource allocation problems.
Dynamic Programming
Opposite approach used for weighted activity selection where greedy fails.
Knowing greedy limits motivates learning dynamic programming for more complex optimization.
Project Management
Scheduling tasks without overlap is a core problem in managing project timelines and resources.
Activity selection algorithms underpin tools like Gantt charts and critical path methods.
Common Pitfalls
#1Selecting activities based on earliest start time instead of earliest finish time.
Wrong approach:Sort activities by start time and pick in that order: activities := []Activity{{1,4},{3,5},{0,6}} sort.Slice(activities, func(i,j int) bool { return activities[i].start < activities[j].start }) // select activities greedily
Correct approach:Sort activities by finish time and select accordingly: sort.Slice(activities, func(i,j int) bool { return activities[i].finish < activities[j].finish }) // select activities greedily
Root cause:Misunderstanding that earliest start time does not guarantee maximum number of activities.
#2Not updating the last selected activity's finish time after selection.
Wrong approach:for _, act := range activities { if act.start >= lastFinish { selected = append(selected, act) // forgot to update lastFinish } }
Correct approach:for _, act := range activities { if act.start >= lastFinish { selected = append(selected, act) lastFinish = act.finish } }
Root cause:Forgetting to track the end time of the last selected activity breaks the selection logic.
#3Assuming greedy works for weighted activities.
Wrong approach:Pick activities greedily by earliest finish time ignoring weights: // This leads to suboptimal total value
Correct approach:// Use dynamic programming to consider weights and compatibility func weightedActivitySelection(...) {...}
Root cause:Overgeneralizing greedy approach beyond its valid problem scope.
Key Takeaways
The Activity Selection Problem finds the maximum number of non-overlapping activities by always choosing the earliest finishing activity first.
Sorting activities by their finish times is essential to apply the greedy selection correctly and efficiently.
Greedy works perfectly for unweighted activities but fails when activities have different values, requiring dynamic programming.
Careful handling of edge cases like equal finish times and updating the last selected finish time is crucial to avoid bugs.
This problem models real-world scheduling challenges and forms a foundation for more complex interval and resource allocation problems.

Practice

(1/5)
1. What is the main goal of the Activity Selection Problem?
easy
A. Find the activity with the longest duration
B. Sort activities by their start times
C. Select the maximum number of activities that don't overlap
D. Calculate the total time of all activities

Solution

  1. Step 1: Understand the problem goal

    The Activity Selection Problem aims to choose activities that do not overlap in time.
  2. Step 2: Identify the objective

    The objective is to select the maximum number of such non-overlapping activities.
  3. Final Answer:

    Select the maximum number of activities that don't overlap -> Option C
  4. Quick Check:

    Goal = Max non-overlapping activities [OK]
Hint: Pick activities with earliest finish times first [OK]
Common Mistakes:
  • Choosing activities based on longest duration
  • Sorting by start time only
  • Trying to maximize total duration instead of count
2. Which of the following Go code snippets correctly sorts activities by their finish time for the Activity Selection Problem?
easy
A. sort.Ints(activities)
B. sort.Slice(activities, func(i, j int) bool { return activities[i].start < activities[j].start })
C. sort.Sort(activities)
D. sort.Slice(activities, func(i, j int) bool { return activities[i].finish < activities[j].finish })

Solution

  1. Step 1: Identify sorting criteria

    Activities must be sorted by their finish time to apply the greedy approach.
  2. Step 2: Check Go sorting syntax

    Using sort.Slice with a comparison function on finish times is correct.
  3. Final Answer:

    sort.Slice(activities, func(i, j int) bool { return activities[i].finish < activities[j].finish }) -> Option D
  4. Quick Check:

    Sort by finish time = sort.Slice(activities, func(i, j int) bool { return activities[i].finish < activities[j].finish }) [OK]
Hint: Sort activities by finish time using sort.Slice [OK]
Common Mistakes:
  • Sorting by start time instead of finish time
  • Using sort.Ints on struct slices
  • Calling sort.Sort without implementing sort.Interface
3. What is the output of the following Go code snippet that implements the Activity Selection Problem?
package main
import (
  "fmt"
  "sort"
)
type Activity struct {
  start, finish int
}
func main() {
  activities := []Activity{{1, 4}, {3, 5}, {0, 6}, {5, 7}, {8, 9}, {5, 9}}
  sort.Slice(activities, func(i, j int) bool { return activities[i].finish < activities[j].finish })
  count := 1
  lastFinish := activities[0].finish
  for i := 1; i < len(activities); i++ {
    if activities[i].start >= lastFinish {
      count++
      lastFinish = activities[i].finish
    }
  }
  fmt.Println(count)
}
medium
A. 4
B. 3
C. 2
D. 5

Solution

  1. Step 1: Sort activities by finish time

    Sorted activities by finish: (1,4), (3,5), (0,6), (5,7), (8,9), (5,9)
  2. Step 2: Select activities greedily

    Select (1,4), next (5,7) since 5 >= 4, then (8,9) since 8 >= 7. Total selected = 3.
  3. Final Answer:

    3 -> Option B
  4. Quick Check:

    Max non-overlapping = 3 [OK]
Hint: Count activities with start >= last selected finish [OK]
Common Mistakes:
  • Counting overlapping activities
  • Not sorting by finish time
  • Starting count from zero instead of one
4. Identify the bug in this Go code snippet for the Activity Selection Problem:
package main
import (
  "fmt"
  "sort"
)
type Activity struct {
  start, finish int
}
func main() {
  activities := []Activity{{1, 3}, {2, 5}, {4, 7}}
  sort.Slice(activities, func(i, j int) bool { return activities[i].start < activities[j].start })
  count := 1
  lastFinish := activities[0].finish
  for i := 1; i < len(activities); i++ {
    if activities[i].start >= lastFinish {
      count++
      lastFinish = activities[i].finish
    }
  }
  fmt.Println(count)
}
medium
A. Sorting by start time instead of finish time
B. Initializing count to zero instead of one
C. Using wrong comparison operator in if condition
D. Not importing the fmt package

Solution

  1. Step 1: Check sorting criteria

    The code sorts activities by start time, but the greedy approach requires sorting by finish time.
  2. Step 2: Understand impact of sorting

    Sorting by start time can cause incorrect selection of activities, leading to wrong count.
  3. Final Answer:

    Sorting by start time instead of finish time -> Option A
  4. Quick Check:

    Sort by finish time needed [OK]
Hint: Always sort by finish time, not start time [OK]
Common Mistakes:
  • Sorting by start time
  • Forgetting to update lastFinish
  • Starting count at zero
5. Given the following activities with start and finish times: [(1, 2), (3, 4), (0, 6), (5, 7), (8, 9), (5, 9)], what is the maximum number of activities that can be selected without overlap using the Activity Selection Problem approach?
hard
A. 4
B. 5
C. 3
D. 6

Solution

  1. Step 1: Sort activities by finish time

    Sorted activities: (1,2), (3,4), (0,6), (5,7), (8,9), (5,9)
  2. Step 2: Select activities greedily

    Select (1,2), then (3,4) since 3 >= 2, then (5,7) since 5 >= 4, then (8,9) since 8 >= 7. Total selected = 4.
  3. Final Answer:

    4 -> Option A
  4. Quick Check:

    Max non-overlapping = 4 [OK]
Hint: Pick earliest finishing activities that start after last finish [OK]
Common Mistakes:
  • Including overlapping activities
  • Not sorting by finish time
  • Counting all activities without checking overlap
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard