Java: Two Best Non-Overlapping Events

Thought Process

The Two Best Non-Overlapping Events problem requires finding the maximum sum of values from two events that don't overlap in time. Here are two approaches:

Approach 1: Greedy with Priority Queue - Sort events by startTime to process chronologically. Use a min-heap to track events by endTime. Maintain maximum value of non-overlapping event (maxVal) while processing. Update answer with maximum of current answer's value & (current event's value + maxVal).

Approach 2: Dynamic Programming with Binary Search - Sort events by endTime to enable binary search for non-overlapping events. Use DP table where t[i][j] represents maximum value with 'j' events from first 'i' events. For each event, use binary search to find latest non-overlapping event and update DP state.

Approach 1: Greedy with Priority Queue

class Solution {
    public int maxTwoEvents(int[][] events) {
        
        // Sort events based on starting time
        Arrays.sort(events,(a,b)->Integer.compare(a[0],b[0]));

        // Min Heap of {endTime,profit}
        PriorityQueue<int[]> pq = new PriorityQueue<>((a,b)-> Integer.compare(a[0],b[0]));

        // Note-1:  We used a min-heap because we need the event with the earliest end time.
        // If the event with the earliest end time overlaps, then all other events will also overlap. 

        // Note-2: maxVal should be declared outside the 'for' loop as it stores the maximum value of 
        // non-overlapping interval encountered so far.

        int ans = 0;
        int maxVal = 0; 
        for(int i=0;i<events.length;i++){

            int startTime = events[i][0];
            int endTime   = events[i][1];
            int profit1   = events[i][2];
            
            // Among all non overlapping event, find the one with best profit.
            while(!pq.isEmpty() && pq.peek()[0] < startTime){

                int profit2 = pq.peek()[1]; // Profit of Non Overlapping Event
                maxVal = Math.max(maxVal,profit2);
                pq.poll();
            }

            ans = Math.max(ans,maxVal + profit1);
            pq.offer(new int[]{endTime,profit1});
        }
        return ans;
    }
    // Note: Above approach is Greedy approach. This problem can also be solved via DP.
}

Approach 2: Dynamic Programming with Binary Search

class Event{

    int startTime;
    int endTime;
    int value;

    Event(int start, int end ,int val){
        this.startTime = start;
        this.endTime = end;
        this.value = val;
    }
}
class Solution {

    // Will use binary search to find the non overlapping event
    // Returns the latest event that ends before the current event starts.
    int srch(List<Event> list, int event){

        int start = 0;
        int end = event-1; // As it is 0-based indexing i.e why end=event-1
        int ans = -1;

        while(start<=end){

            int mid = (start+end)/2;
            if(list.get(mid).endTime < list.get(event).startTime){
                ans = mid;
                start = mid+1;
            }
            else
                end = mid-1;

        }
        return ans;
    }
    public int maxTwoEvents(int[][] events) {
        
        int noOfEvents = events.length;
        List<Event> list = new ArrayList<>();
        for(int i=0;i<noOfEvents;i++)
            list.add(new Event(events[i][0],events[i][1],events[i][2]));
        
        // Sorting by endTime ensures that for any event i, all possible non-overlapping
        // events (ending before ith event) appear before it in sorted order.
        Collections.sort(list, (a,b)->Integer.compare(a.endTime,b.endTime));

        int[][] ans = new int[noOfEvents+1][3];

        for(int i=0;i<=noOfEvents;i++){
            for(int j=0;j<=2;j++){

                // If number of events = 0 OR no elements selected
                if(i==0 || j==0)
                    ans[i][j] = 0;

                else{

                    int include = list.get(i-1).value;
                    int prevNonOverlappingEvent = srch(list,i-1);
                    if(prevNonOverlappingEvent!=-1)
                        include += ans[prevNonOverlappingEvent+1][j-1];

                    int exclude = ans[i-1][j];

                    ans[i][j] = Math.max(include,exclude);
                    // Note: Above srch may miss a higher-value non-overlapping event & just returns
                    // latest event that ends before the current event starts but the DP accounts 
                    // for it by keeping track of the best value achievable so far.
                }
            }
        }
        return ans[noOfEvents][2];
    }
}

/*
Golden Note:

1. Sort by endTime when we need to efficiently find the latest non-overlapping event.
2. Sort by startTime when we merge intervals or checking overlaps.
*/

Code Complexity

Time Complexity: O(n log n) (Both)

Greedy approach has sorting (n log n) and heap operations (n log n). DP approach has sorting (n logn) and binary searches (n log n).

Space Complexity: O(n) (Both)

Greedy uses heap space (O(n)). DP uses additional space for sorted events and DP table (O(n)).

Code copied!