Permutations IV
HardPrompt
Given two integers, n and k, an alternating permutation is a permutation of the first n positive integers such that no two adjacent elements are both odd or both even.
Return the k-th alternating permutation sorted in lexicographical order. If there are fewer than k valid alternating permutations, return an empty list.
Example 1:
Input: n = 4, k = 6
Output: [3,4,1,2]
Explanation:
The lexicographically-sorted alternating permutations of [1, 2, 3, 4] are:
[1, 2, 3, 4][1, 4, 3, 2][2, 1, 4, 3][2, 3, 4, 1][3, 2, 1, 4][3, 4, 1, 2]← 6th permutation[4, 1, 2, 3][4, 3, 2, 1]
Since k = 6, we return [3, 4, 1, 2].
Example 2:
Input: n = 3, k = 2
Output: [3,2,1]
Explanation:
The lexicographically-sorted alternating permutations of [1, 2, 3] are:
[1, 2, 3][3, 2, 1]← 2nd permutation
Since k = 2, we return [3, 2, 1].
Example 3:
Input: n = 2, k = 3
Output: []
Explanation:
The lexicographically-sorted alternating permutations of [1, 2] are:
[1, 2][2, 1]
There are only 2 alternating permutations, but k = 3, which is out of range. Thus, we return an empty list [].
Constraints:
1 <= n <= 1001 <= k <= 1015
Approaches
3 approaches with complexity analysis and trade-offs.
The most straightforward but inefficient approach is to generate every possible permutation of the numbers from 1 to n. For each generated permutation, we check if it satisfies the alternating property. We collect all such valid permutations, sort them lexicographically, and then pick the k-th one from the sorted list.
Algorithm
- Generate all permutations of numbers from 1 to
n. - For each permutation, write a helper function to check if it is an alternating permutation (i.e., no two adjacent elements have the same parity).
- Store all valid alternating permutations in a list.
- Sort this list lexicographically.
- If
kis within the bounds of the list's size, return the(k-1)-th element. - Otherwise, return an empty list.
Walkthrough
This method involves a brute-force enumeration of all possibilities. We can use a standard backtracking algorithm to generate all n! permutations. For each complete permutation, a simple loop can verify the alternating condition by checking the parity of adjacent elements. The valid permutations are stored. After all permutations have been checked, the list of valid ones is sorted. Finally, we access the k-th element. Given the constraint n <= 100, n! is astronomically large, so this approach will time out for most of the test cases.
// This is a conceptual illustration and is not a feasible solution.import java.util.*; class Solution { public List<Integer> getPermutation(int n, int k) { List<List<Integer>> validPerms = new ArrayList<>(); List<Integer> currentPerm = new ArrayList<>(); boolean[] used = new boolean[n + 1]; generatePermutations(n, currentPerm, used, validPerms); // The validPerms list is already sorted lexicographically if generated in order. if (k > validPerms.size()) { return new ArrayList<>(); } return validPerms.get(k - 1); } private void generatePermutations(int n, List<Integer> currentPerm, boolean[] used, List<List<Integer>> validPerms) { if (currentPerm.size() == n) { if (isAlternating(currentPerm)) { validPerms.add(new ArrayList<>(currentPerm)); } return; } for (int i = 1; i <= n; i++) { if (!used[i]) { currentPerm.add(i); used[i] = true; generatePermutations(n, currentPerm, used, validPerms); used[i] = false; currentPerm.remove(currentPerm.size() - 1); } } } private boolean isAlternating(List<Integer> perm) { for (int i = 0; i < perm.size() - 1; i++) { if ((perm.get(i) % 2) == (perm.get(i + 1) % 2)) { return false; } } return true; }}Complexity
Time
O(n! * n). Generating all `n!` permutations takes `O(n! * n)` time, and checking each one takes `O(n)`. This is computationally prohibitive.
Space
O(A_n * n), where A_n is the number of alternating permutations. This can be very large.
Trade-offs
Pros
Simple to understand and implement.
Cons
Extremely high time complexity, making it infeasible for
ngreater than about 10.High space complexity as it needs to store all valid permutations, which can be a very large number.
A slightly improved approach uses backtracking with pruning. Instead of generating all permutations and then filtering, we check the alternating property at each step of the permutation's construction. If adding a number violates the property, we immediately discard that path (prune the search tree). This avoids exploring invalid branches. However, this method still needs to generate and store all valid alternating permutations before finding the k-th one.
Algorithm
- Use a backtracking approach to build permutations step-by-step.
- At each step of the recursion, when considering adding a number, check if it maintains the alternating property with the previously added number.
- If it does, proceed with the recursion. If not, prune this path.
- Collect all fully formed valid alternating permutations.
- Since the numbers are tried in increasing order, the resulting list of permutations will be lexicographically sorted.
- Return the
(k-1)-th permutation from the list. Ifkis too large, return an empty list.
Walkthrough
We can define a recursive function, say findAlternating(currentPerm, usedNumbers). In this function, we loop through numbers 1 to n. If a number hasn't been used, we check its parity against the last number in currentPerm. If it's the first number or if the parities are different, we add it to currentPerm, mark it as used, and recurse. When a permutation of length n is formed, it's added to a results list. This is more efficient than the pure brute-force method but still impractical because the number of alternating permutations, A_n, grows very quickly (e.g., A_20 is in the trillions), exceeding time and memory limits.
// This is a conceptual illustration and is not a feasible solution for the given constraints.import java.util.*; class Solution { List<List<Integer>> validPerms = new ArrayList<>(); public List<Integer> getPermutation(int n, int k) { List<Integer> currentPerm = new ArrayList<>(); boolean[] used = new boolean[n + 1]; findAlternating(n, k, currentPerm, used); if (k > validPerms.size()) { // This check is not optimal, k can be large return new ArrayList<>(); } // A better way would be to stop after finding k permutations. return validPerms.get(k - 1); } private void findAlternating(int n, int k, List<Integer> currentPerm, boolean[] used) { if (validPerms.size() >= k) { // Stop early if we have enough return; } if (currentPerm.size() == n) { validPerms.add(new ArrayList<>(currentPerm)); return; } for (int i = 1; i <= n; i++) { if (!used[i]) { if (currentPerm.isEmpty() || (currentPerm.get(currentPerm.size() - 1) % 2 != i % 2)) { currentPerm.add(i); used[i] = true; findAlternating(n, k, currentPerm, used); used[i] = false; currentPerm.remove(currentPerm.size() - 1); } } } }}Complexity
Time
O(A_n * n), where A_n is the number of alternating permutations. This is still too slow.
Space
O(A_n * n), to store all valid permutations.
Trade-offs
Pros
More efficient than the first approach by avoiding the generation of invalid permutations.
Cons
While better than brute-force, it still generates and stores all valid permutations.
The number of alternating permutations can be very large, leading to excessive memory usage and time, especially for larger
n.
The most efficient solution involves constructing the k-th permutation directly without generating all possibilities. This is analogous to the standard k-th permutation problem but adapted for the alternating constraint. We determine the numbers of the permutation one by one, from left to right. At each position, we decide which number to place by calculating how many valid alternating permutations would start with that prefix. By comparing k with these counts, we can greedily choose the correct number for each position.
Algorithm
- Precomputation: Calculate factorials up to
n. Since the results can be large, cap them at a value likek+1to prevent overflow, as any count larger thankis equivalent for our purpose. - Initialization: Create lists of available odd and even numbers. Convert
kto be 0-indexed (k--). - Total Count Check: Calculate the total number of alternating permutations. If
kis greater than this total, return an empty list. - Constructive Loop: Iterate from
i = 0ton-1to determine the number at each position of the result array.- In each iteration
i, iterate through the available numbers (both odd and even ifi=0, or only of the required parity ifi>0) in increasing lexicographical order. - For each candidate number, calculate how many valid alternating permutations can be formed with the remaining numbers. This count is
fact[rem_odds] * fact[rem_evens]. - If
kis greater than or equal to this count, it means the desired permutation is not in this block. Subtract the count fromkand try the next available number. - If
kis less than the count, the number for the current position is found. Add it to the result, remove it from the available numbers list, and break the inner loop to proceed to the next position.
- In each iteration
- Return: Return the constructed permutation.
Walkthrough
The core idea is to determine each digit of the permutation sequentially. To decide the first digit, we can try placing 1, then 2, and so on. For each choice, we calculate how many valid alternating permutations can be formed with the remaining numbers.
Let's say we have o odd numbers and e even numbers left. If we need to place an odd number next, the number of ways to form a valid alternating sequence with the remaining o-1 odd and e even numbers (which must start with an even number) is given by a combinatorial formula. An alternating permutation of o odds and e evens is possible only if |o - e| <= 1. The number of ways to arrange them is o! * e!. If o != e, the starting parity is fixed. If o == e, either parity can start, giving 2 * o! * e! total permutations.
We can define a helper function countPerms(o, e, nextIsOdd) that calculates the number of valid permutations given the remaining counts and the required parity of the next number. This count is essentially fact[o] * fact[e] if the parity constraints are met (o must be e or e+1 if nextIsOdd is true, and e must be o or o+1 if nextIsOdd is false).
Since k and the factorial products can be very large, we must handle potential overflows. We can cap all calculations at k+1, as we only need to know if a block of permutations is smaller or larger than the current k.
The algorithm proceeds as follows: for each position in the permutation, we iterate through the available numbers in increasing order. We check if the number's parity is valid for the current position. If it is, we calculate the number of completions (count). If k >= count, we skip this block of permutations by setting k -= count and trying the next available number. Otherwise, we've found our digit. We place it in the result, remove it from the available pool, and move to the next position with the updated k.
import java.util.*; class Solution { public int[] alternatingPermutation(int n, long k) { long[] fact = new long[n + 1]; fact[0] = 1; // Use k as the cap to prevent overflow. Any value > k is treated as k. long cap = k; for (int i = 1; i <= n; i++) { fact[i] = safeMul(fact[i - 1], i, cap); } List<Integer> odds = new ArrayList<>(); List<Integer> evens = new ArrayList<>(); for (int i = 1; i <= n; i++) { if (i % 2 != 0) { odds.add(i); } else { evens.add(i); } } long oddStartCount = countPerms(odds.size(), evens.size(), true, fact, cap); long evenStartCount = countPerms(evens.size(), odds.size(), true, fact, cap); long totalPerms = safeAdd(oddStartCount, evenStartCount, cap); if (k > totalPerms) { return new int[0]; } k--; // Convert to 0-indexed int[] result = new int[n]; boolean lastWasOdd = false; // Doesn't matter for the first element for (int i = 0; i < n; i++) { int oddIdx = 0; int evenIdx = 0; while (true) { boolean tryOdd = false; boolean tryEven = false; // Determine which number to try next to maintain lexicographical order if (i == 0) { if (oddIdx < odds.size() && evenIdx < evens.size()) { if (odds.get(oddIdx) < evens.get(evenIdx)) tryOdd = true; else tryEven = true; } else if (oddIdx < odds.size()) tryOdd = true; else tryEven = true; } else { if (!lastWasOdd) tryOdd = true; else tryEven = true; } if (tryOdd) { long count = countPerms(odds.size() - 1, evens.size(), false, fact, cap); if (k >= count) { k -= count; oddIdx++; } else { result[i] = odds.remove(oddIdx); lastWasOdd = true; break; } } else { // tryEven long count = countPerms(evens.size() - 1, odds.size(), true, fact, cap); if (k >= count) { k -= count; evenIdx++; } else { result[i] = evens.remove(evenIdx); lastWasOdd = false; break; } } } } return result; } // Counts permutations of `count1` items of one parity and `count2` of another, // where the next item must be of the first parity. private long countPerms(int count1, int count2, boolean isCount1Turn, long[] fact, long cap) { if (isCount1Turn) { // Permutation must start with an item from the `count1` group if (count1 < count2 || count1 > count2 + 1) return 0; return safeMul(fact[count1], fact[count2], cap); } else { // Permutation must start with an item from the `count2` group if (count2 < count1 || count2 > count1 + 1) return 0; return safeMul(fact[count1], fact[count2], cap); } } private long safeMul(long a, long b, long cap) { if (a == 0 || b == 0) return 0; if (b > 0 && a > cap / b) return cap; return a * b; } private long safeAdd(long a, long b, long cap) { if (a >= cap || b >= cap || a + b >= cap) return cap; return a + b; }}Complexity
Time
O(n^3). The main loop runs `n` times. The inner loop can run up to `n` times. Inside, `ArrayList.remove(index)` takes O(n) time. This can be optimized to O(n log n) using a data structure like a Fenwick tree (BIT) to find and remove the k-th available number in O(log n) time.
Space
O(n) for storing the lists of available numbers, the result, and the factorials.
Trade-offs
Pros
Highly efficient and can handle large
nandkwithin the given constraints.Does not require storing a large number of permutations, leading to low space complexity.
Cons
The logic is more complex than naive approaches.
Implementation requires careful handling of large numbers and indices.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.