questions M-R

This commit is contained in:
2025-05-25 12:43:25 +01:00
parent ad320dc703
commit 0a0feb93b5
62 changed files with 12841 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
title: Maximum Length of Pair Chain
slug: maximum-length-of-pair-chain
difficulty: medium
leetcode_id: 646
leetcode_url: https://leetcode.com/problems/maximum-length-of-pair-chain/
categories:
- arrays
- sorting
- dynamic-programming
patterns:
- greedy
- intervals
description: |
You are given an array of `n` pairs `pairs` where `pairs[i] = [left_i, right_i]` and `left_i < right_i`.
A pair `p2 = [c, d]` **follows** a pair `p1 = [a, b]` if `b < c`. A **chain** of pairs can be formed in this fashion.
Return *the length of the longest chain which can be formed*.
You do not need to use up all the given intervals. You can select pairs in any order.
constraints: |
- `n == pairs.length`
- `1 <= n <= 1000`
- `-1000 <= left_i < right_i <= 1000`
examples:
- input: "pairs = [[1,2],[2,3],[3,4]]"
output: "2"
explanation: "The longest chain is [1,2] -> [3,4]. Note that [1,2] -> [2,3] is invalid because 2 is not strictly less than 2."
- input: "pairs = [[1,2],[7,8],[4,5]]"
output: "3"
explanation: "The longest chain is [1,2] -> [4,5] -> [7,8]."
explanation:
intuition: |
Imagine you're scheduling non-overlapping meetings in a conference room, but with a twist — between each meeting, you need at least one time unit of buffer. Your goal is to fit as many meetings as possible.
The key insight is that **greedily picking pairs that end earliest** leaves the most room for subsequent pairs. If you always choose the pair with the smallest ending value (right bound) that can legally follow the current chain, you maximise your opportunities to extend the chain further.
Think of it like this: given two pairs that could both extend your current chain, the one ending earlier gives you more flexibility for what comes next. This is the classic **activity selection** problem from algorithm theory.
Why does greedy work here? Because the "follow" relationship only depends on the ending value of the previous pair. By sorting on end values and greedily extending, we make locally optimal choices that lead to the globally optimal solution.
approach: |
We solve this using the **Greedy Approach** (sorting by end values):
**Step 1: Sort pairs by their ending value**
- Sort the `pairs` array by each pair's right (ending) element
- This ensures we always consider pairs that "finish earliest" first
- Pairs that end earlier leave more room for subsequent pairs
&nbsp;
**Step 2: Initialise tracking variables**
- `chain_length`: Set to `1` since we'll always include at least the first pair
- `current_end`: Set to the end value of the first pair after sorting
&nbsp;
**Step 3: Iterate through the remaining pairs**
- For each pair starting from index 1, check if its start value is greater than `current_end`
- If `pair[0] > current_end`, this pair can extend the chain:
- Increment `chain_length` by 1
- Update `current_end` to this pair's end value
- If not, skip this pair — it overlaps with our current chain end
&nbsp;
**Step 4: Return the result**
- Return `chain_length` as the maximum chain length
&nbsp;
This greedy approach works because by always choosing the pair that ends earliest, we maximise the remaining space for future pairs, leading to the longest possible chain.
common_pitfalls:
- title: Sorting by Start Value Instead of End Value
description: |
A common mistake is sorting by the start value (left bound) instead of the end value.
Consider `pairs = [[1,5],[2,3],[4,6]]`:
- Sorted by start: `[[1,5],[2,3],[4,6]]` → Chain `[1,5] -> [4,6]` = length 2
- Sorted by end: `[[2,3],[1,5],[4,6]]` → Chain `[2,3] -> [4,6]` = length 2
But with `[[1,10],[2,3],[4,5]]`:
- Sorted by start: `[[1,10],[2,3],[4,5]]` → Only `[1,10]` = length 1
- Sorted by end: `[[2,3],[4,5],[1,10]]` → Chain `[2,3] -> [4,5]` = length 2
The pair `[1,10]` "blocks" everything when chosen first, but sorting by end value reveals that `[2,3]` and `[4,5]` fit together.
wrong_approach: "Sort by left_i (start value)"
correct_approach: "Sort by right_i (end value)"
- title: Using >= Instead of > for the Follow Condition
description: |
The problem states `p2` follows `p1` if `b < c` (strictly less than). Using `<=` would incorrectly allow pairs like `[1,2]` to be followed by `[2,3]`.
With `pairs = [[1,2],[2,3]]`:
- Correct (`b < c`): `[1,2]` cannot be followed by `[2,3]` because `2 < 2` is false
- Wrong (`b <= c`): Would incorrectly chain them
Always use strict inequality: `pair[0] > current_end`.
wrong_approach: "Check if pair[0] >= current_end"
correct_approach: "Check if pair[0] > current_end (strict inequality)"
- title: Using Dynamic Programming When Greedy Suffices
description: |
This problem can be solved with DP (O(n^2) time), but the greedy approach is more efficient (O(n log n) due to sorting).
While DP works by computing `dp[i]` = longest chain ending at pair `i`, the greedy approach recognises that we only need to track the current chain's end value. The insight that "ending earliest is always best" eliminates the need to consider all possible previous pairs.
With `n <= 1000`, both approaches pass, but greedy is cleaner and faster.
wrong_approach: "O(n^2) DP computing all subproblems"
correct_approach: "O(n log n) greedy with sorting"
key_takeaways:
- "**Activity selection pattern**: When selecting non-overlapping intervals, sort by end time and greedily pick the earliest-ending valid option"
- "**Greedy vs DP**: Recognise when greedy provides the optimal solution — here, the locally optimal choice (earliest end) leads to the global optimum"
- "**Sorting as a preprocessing step**: Many interval problems become tractable once sorted by start or end values"
- "**Related problems**: This pattern applies to Meeting Rooms II, Non-overlapping Intervals, and Minimum Number of Arrows to Burst Balloons"
time_complexity: "O(n log n). Sorting dominates the time complexity. The subsequent single pass through the sorted array is O(n)."
space_complexity: "O(1) or O(n). Depends on the sorting implementation — in-place sorting uses O(1) extra space, while Python's Timsort uses O(n) auxiliary space."
solutions:
- approach_name: Greedy (Sort by End Value)
is_optimal: true
code: |
def find_longest_chain(pairs: list[list[int]]) -> int:
# Sort pairs by their ending value (right bound)
# This ensures we always consider pairs that "finish earliest" first
pairs.sort(key=lambda x: x[1])
# Start with the first pair in our chain
chain_length = 1
current_end = pairs[0][1]
# Try to extend the chain with each subsequent pair
for i in range(1, len(pairs)):
# Can this pair follow our current chain?
# The start must be strictly greater than our chain's end
if pairs[i][0] > current_end:
chain_length += 1
current_end = pairs[i][1] # Update chain end
return chain_length
explanation: |
**Time Complexity:** O(n log n) — Sorting dominates; the linear scan is O(n).
**Space Complexity:** O(1) extra space (ignoring the space used by sorting).
By sorting pairs by their end values, we ensure that greedily picking the first valid pair always leads to the optimal solution. This is the classic activity selection algorithm.
- approach_name: Dynamic Programming
is_optimal: false
code: |
def find_longest_chain(pairs: list[list[int]]) -> int:
# Sort pairs by start value for DP approach
pairs.sort(key=lambda x: x[0])
n = len(pairs)
# dp[i] = longest chain ending with pairs[i]
dp = [1] * n
# For each pair, check all previous pairs
for i in range(1, n):
for j in range(i):
# Can pairs[j] be followed by pairs[i]?
if pairs[j][1] < pairs[i][0]:
dp[i] = max(dp[i], dp[j] + 1)
# Return the maximum chain length
return max(dp)
explanation: |
**Time Complexity:** O(n^2) — Nested loops comparing all pairs.
**Space Complexity:** O(n) — DP array storing chain lengths.
This approach computes the longest chain ending at each pair by checking all previous pairs. While correct, it's less efficient than the greedy approach. Included to show the DP perspective and for cases where greedy intuition isn't immediately clear.