Start learning free

LESSON 270 OF 348 · BACKTRACKING

Generate Valley Permutations in Kotlin: Bitmasks Over 2^(n-1) Choices

Practice O(n!) Read the full explanation

Given a non-negative integer n, return all the valley permutations of length n.

A valley permutation is defined as a permutation that is a concatenation of two parts: a monotonically decreasing sequence and then a monotonically increasing sequence.

E.g. valley permutations follow this format: [p[0] < p[1] < p[2] < ... < p[k] > p[k+1] > p[k + 2] > ... > p[n]].


Example:

Input: n = 4

Output:[
    [1, 2, 3, 4],
    [2, 1, 3, 4],
    [3, 1, 2, 4],
    [3, 2, 1, 4],
    [4, 1, 2, 3],
    [4, 2, 1, 3],
    [4, 3, 1, 2],
    [4, 3, 2, 1]
]

Explanation:
Permutations suchs as [1, 4, 2, 3] are not valley because 1 < 4 > 2 < 3

Notes: n <= 10


STEP 1 OF 9
    Ctrl + Enter to check

    KOTLIN · BACKTRACKING

    Generate Valley Permutations in Kotlin, explained

    Problem overview

    The function generateValleyPermutations takes a non-negative integer n and returns every valley permutation of the numbers 1 to n. A valley permutation first decreases and then increases: it walks down to a lowest point and climbs back up. The result is a MutableList<IntArray>, one array per permutation.

    For n = 4 there are eight valleys: [1, 2, 3, 4], [2, 1, 3, 4], [3, 1, 2, 4], [3, 2, 1, 4], [4, 1, 2, 3], [4, 2, 1, 3], [4, 3, 1, 2] and [4, 3, 2, 1]. A permutation such as [1, 4, 2, 3] is not a valley: it rises, falls and rises again.

    Approach: the smallest value sits at the bottom

    The obvious plan is to generate all n! permutations and keep the valleys. That works for tiny inputs and wastes almost all of its effort, since only a few permutations pass the filter. A better plan builds only valleys.

    The key observation is that the lowest point of the valley must be the value 1. Every other value, 2 to n, then lies either on the descending side to the left of 1 or on the ascending side to the right. Once you decide the side of each value, the permutation is fixed: the left values must appear in decreasing order and the right values in increasing order, so there is no further freedom. With n - 1 values and two sides each, there are exactly 2^(n - 1) valleys. For n = 4 that is 8, matching the list above.

    A binary number with n - 1 bits can record all these choices. Counting mask from 0 to 2^(n - 1) - 1 visits every combination of bits once. Bit i of the mask is the side of the value i + 2: 1 means left, 0 means right. For n = 3 the masks give [1, 2, 3] for 0, [2, 1, 3] for 1, [3, 1, 2] for 2 and [3, 2, 1] for 3.

    Step by step

    1. Create the result list. It starts empty and collects one IntArray per mask.
    2. Loop over all masks from 0 up to (1 shl (n - 1)) - 1. Shifting 1 left by k places gives 2^k, so this is every combination of n - 1 bits.
    3. Make fresh left and right lists inside that loop. Declaring them there gives each mask empty lists, so nothing leaks from the previous mask.
    4. Loop over the bits with i from 0 to n - 2. That is n - 1 bits, one for each of the values 2 to n.
    5. Compute the value this bit controls: i + 2. Value 1 needs no bit because it always sits at the bottom. The variable is named value and not val because val is a Kotlin keyword.
    6. Send the value to a side. If bit i is set, it goes to left; otherwise to right.
    7. Sort the sides. left must decrease toward 1 and right must increase away from it.
    8. Assemble the permutation. Fill an IntArray of size n with the left values, then 1, then the right values, tracking the next free position in idx. Add the array to result.
    9. Return result after the outer loop, once every mask has produced its permutation.

    The solution in Kotlin

    class Solution {
        fun generateValleyPermutations(n: Int): MutableList<IntArray> {
            val result = mutableListOf<IntArray>()
            for (mask in 0 until (1 shl (n - 1))) {
                val left = mutableListOf<Int>()
                val right = mutableListOf<Int>()
                for (i in 0 until n - 1) {
                    val value = i + 2
                    if ((mask shr i) and 1 == 1) {
                        left.add(value)
                    } else {
                        right.add(value)
                    }
                }
                left.sortDescending()
                right.sort()
                val perm = IntArray(n)
                var idx = 0
                for (v in left) {
                    perm[idx++] = v
                }
                perm[idx++] = 1
                for (v in right) {
                    perm[idx++] = v
                }
                result.add(perm)
            }
            return result
        }
    }
    

    The Kotlin details worth a closer look:

    • Bit operations are named functions. Kotlin has no <<, >> or & operators for integers. It uses the infix functions shl, shr and and instead.
    • Precedence. Named infix functions bind tighter than ==, so (mask shr i) and 1 == 1 means ((mask shr i) and 1) == 1: shift bit i to the lowest position, keep only it, and compare with 1. The comparison has to be written out, because an if condition must be a Boolean; an Int such as 1 is not accepted as true.
    • until excludes its upper bound. 0 until n - 1 covers 0 to n - 2. The range 0..n - 1 would include n - 1 and read one bit too many.
    • In-place sorting. On a MutableList, sort() and sortDescending() reorder the list itself and return Unit. Since the values were added in increasing order, right is already ascending and left is ascending too, so only left really needs reversing. Both calls make the intent explicit.
    • perm[idx++] = v stores at the current position and then moves idx forward by one. idx is a var; the lists are val references to mutable lists, so add still works.
    • IntArray versus List<Int>. IntArray(n) is a primitive array filled with zeros, fixed in size. The lists hold boxed Int values and grow as needed, which suits collecting the sides before their sizes are known.

    Complexity

    Time: O(2^n * n log n). There are 2^(n - 1) masks. Each one loops over n - 1 bits, sorts lists of at most n - 1 values and copies n values into an array. The output itself holds n numbers per permutation, so no algorithm can do better than O(n * 2^n).

    Space: O(n * 2^n). The result holds 2^(n - 1) arrays of n values. Apart from the output, the working lists need O(n). With n at most 10, that is 512 arrays of 10 numbers.

    Edge cases and common mistakes

    • n = 1. The loop runs for the single mask 0, nothing goes to either side, and the result is [[1]].
    • n = 2. The two valleys are [1, 2] and [2, 1]; both sequences have one of the two parts empty, which is allowed.
    • n = 0. 1 shl (n - 1) becomes 1 shl -1, and Kotlin uses only the low five bits of the shift distance, so this is 1 shl 31, a negative number. The loop range 0 until that number is empty, so the function returns an empty list.
    • Reusing the side lists. Declaring left and right outside the mask loop would mix values from different masks.
    • Forgetting the descending order. Without sorting the left side in decreasing order, the permutation would rise before 1 instead of falling to it, and it would not be a valley.
    • Reading the wrong bit count. Looping i to n - 1 adds a value n + 1 that does not belong in the permutation, and perm overflows.

    Testing

    • n = 1 gives [[1]].
    • n = 2 gives [[1, 2], [2, 1]].
    • n = 3 gives [[1, 2, 3], [2, 1, 3], [3, 1, 2], [3, 2, 1]].
    • n = 4 gives the eight arrays listed at the top, in that order.
    • n = 5 gives 16 permutations.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free