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,202 @@
title: Permutations
slug: permutations
difficulty: medium
leetcode_id: 46
leetcode_url: https://leetcode.com/problems/permutations/
categories:
- arrays
- recursion
patterns:
- backtracking
description: |
Given an array `nums` of distinct integers, return *all the possible permutations*. You can return the answer in **any order**.
constraints: |
- `1 <= nums.length <= 6`
- `-10 <= nums[i] <= 10`
- All the integers of `nums` are **unique**
examples:
- input: "nums = [1,2,3]"
output: "[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]"
explanation: "All 6 permutations of the three distinct integers."
- input: "nums = [0,1]"
output: "[[0,1],[1,0]]"
explanation: "Both orderings of two elements."
- input: "nums = [1]"
output: "[[1]]"
explanation: "A single element has only one permutation."
explanation:
intuition: |
Imagine you have a set of numbered cards and want to arrange them in every possible order on a table.
For each position in the arrangement, you pick one card from the remaining unused cards, place it, then move to the next position. Once all positions are filled, you have one complete permutation. To find *all* permutations, you systematically try every possible choice at each position.
Think of it like a decision tree: at each level, you branch out by choosing one of the remaining elements. When you reach a leaf (all elements placed), you've found a valid permutation. Then you **backtrack** — undo your last choice and try a different one.
This is the essence of backtracking: build a solution incrementally, and when you complete one solution or hit a dead end, reverse your last decision to explore other possibilities.
approach: |
We solve this using **Backtracking**:
**Step 1: Set up the recursive function**
- Create a helper function that takes the current partial permutation being built
- Maintain a way to track which elements have been used (either via a set, or by swapping elements in-place)
&nbsp;
**Step 2: Base case**
- When the current permutation has the same length as the input array, we've placed all elements
- Add a copy of the current permutation to our results list
&nbsp;
**Step 3: Recursive case — explore all choices**
- For each element in `nums` that hasn't been used yet:
- **Choose**: Add the element to the current permutation, mark it as used
- **Explore**: Recursively call the function to fill the next position
- **Unchoose (Backtrack)**: Remove the element from the current permutation, mark it as unused
&nbsp;
**Step 4: Return all collected permutations**
- After the recursion completes, the results list contains all valid permutations
&nbsp;
The backtracking pattern ensures we systematically explore every possible ordering without repetition.
common_pitfalls:
- title: Forgetting to Backtrack
description: |
After exploring a branch, you must undo your choice before trying the next option. If you forget to remove the element from the current path or unmark it as used, subsequent branches will have incorrect state.
For example, if you add `2` to the path and recurse, you must remove `2` after returning — otherwise the next iteration might try to build `[1, 2, 3, 2]` which is invalid.
wrong_approach: "Only adding elements without removing them after recursion"
correct_approach: "Add element, recurse, then remove element (backtrack)"
- title: Storing References Instead of Copies
description: |
When you find a valid permutation and add it to results, you must add a **copy** of the current path, not a reference to it.
If you do `results.append(current_path)` without copying, all entries in results will point to the same list object, which keeps changing as you backtrack. You'll end up with all identical (and likely empty) lists.
wrong_approach: "results.append(current_path)"
correct_approach: "results.append(current_path.copy()) or results.append(list(current_path))"
- title: Not Handling the Used Set Correctly
description: |
When using a set to track used elements, make sure to:
- Add the element to `used` before recursing
- Remove it from `used` after recursing
Alternatively, you can use the element's index instead of its value, or use an in-place swap approach to avoid the set entirely.
key_takeaways:
- "**Backtracking template**: Choose, explore, unchoose — this pattern applies to permutations, combinations, subsets, and many constraint satisfaction problems"
- "**Time complexity is inherently factorial**: Generating all permutations of `n` elements requires O(n!) work since there are n! permutations, each of length n"
- "**In-place swapping alternative**: Instead of tracking used elements, you can swap elements to the 'current position' and recurse on the remaining portion"
- "**Foundation for harder problems**: Permutations II (with duplicates) builds directly on this, adding skip logic for repeated elements"
time_complexity: "O(n * n!). There are n! permutations, and we spend O(n) time copying each one to the result."
space_complexity: "O(n). The recursion stack depth is n, and the current path stores up to n elements. The output list of n! permutations is typically not counted in auxiliary space."
solutions:
- approach_name: Backtracking with Used Set
is_optimal: true
code: |
def permute(nums: list[int]) -> list[list[int]]:
results = []
def backtrack(current_path: list[int], used: set[int]) -> None:
# Base case: permutation is complete
if len(current_path) == len(nums):
results.append(current_path.copy()) # Must copy!
return
# Try each unused element
for num in nums:
if num in used:
continue # Skip already-used elements
# Choose
current_path.append(num)
used.add(num)
# Explore
backtrack(current_path, used)
# Unchoose (backtrack)
current_path.pop()
used.remove(num)
backtrack([], set())
return results
explanation: |
**Time Complexity:** O(n * n!) — We generate all n! permutations, and copying each permutation takes O(n).
**Space Complexity:** O(n) — The recursion depth is n, plus the current path and used set each hold at most n elements.
This approach uses a set to track which elements have been included in the current permutation. At each step, we iterate through all elements and skip those already used.
- approach_name: Backtracking with In-Place Swaps
is_optimal: true
code: |
def permute(nums: list[int]) -> list[list[int]]:
results = []
def backtrack(start: int) -> None:
# Base case: all positions filled
if start == len(nums):
results.append(nums.copy())
return
# Try each element from start to end in the current position
for i in range(start, len(nums)):
# Swap element at i into the start position
nums[start], nums[i] = nums[i], nums[start]
# Recurse to fill remaining positions
backtrack(start + 1)
# Swap back to restore original order
nums[start], nums[i] = nums[i], nums[start]
backtrack(0)
return results
explanation: |
**Time Complexity:** O(n * n!) — Same as the first approach.
**Space Complexity:** O(n) — Only the recursion stack, no additional data structures for tracking used elements.
This approach avoids the `used` set by swapping elements in place. The portion `nums[0:start]` represents the current partial permutation, and we try placing each element from `nums[start:]` at position `start` by swapping.
- approach_name: Iterative (Build Permutations Step by Step)
is_optimal: false
code: |
def permute(nums: list[int]) -> list[list[int]]:
# Start with an empty permutation
result = [[]]
for num in nums:
new_result = []
# For each existing partial permutation
for perm in result:
# Insert num at every possible position
for i in range(len(perm) + 1):
new_perm = perm[:i] + [num] + perm[i:]
new_result.append(new_perm)
result = new_result
return result
explanation: |
**Time Complexity:** O(n * n!) — Same overall, though with different constant factors.
**Space Complexity:** O(n * n!) — We store all intermediate permutations, not just the final ones.
This iterative approach builds permutations by inserting each new number into all possible positions of existing permutations. While it avoids recursion, it uses more memory because it stores all partial results at each stage.