Start learning free

LESSON 264 OF 338 · BACKTRACKING

Generate Valley Permutations in Go: Enumerating with Bitmasks

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

    GO · BACKTRACKING

    Generate Valley Permutations in Go, explained

    Problem overview

    The method generateValleyPermutations receives n and returns every valley permutation of the numbers 1 to n as a [][]int. A valley permutation goes down and then goes up: a strictly decreasing run, the minimum, and then a strictly increasing run. Either run may be empty, so [1, 2, 3, 4] (nothing before the 1) and [4, 3, 2, 1] (nothing after it) both count.

    For n = 4 the answer has eight permutations: [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]. The permutation [1, 4, 2, 3] is not a valley, because it rises, falls and rises again.

    Approach: every value chooses a side

    Trying all n! permutations and testing each one would work for small n, but the valley shape allows a direct construction. In a valley, the smallest value, 1, must be the bottom. Everything to its left is larger and decreasing toward it, and everything to its right is larger and increasing away from it.

    So choose a side for each of the other values 2 to n. Once the values on the left are chosen, their order is forced (descending), and the same holds for the right (ascending). Each choice of sides gives exactly one valley, and different choices give different permutations. With n - 1 values and two sides each, there are 2^(n-1) valleys: 8 for n = 4, matching the example.

    A bitmask enumerates the choices. Count mask from 0 to 2^(n-1) - 1; bit i of the mask says where value i + 2 goes: a set bit means left, a clear bit means right. For n = 4, the mask 101 in binary puts the values 2 and 4 on the left and 3 on the right, which gives [4 2 1 3].

    Step by step

    1. Create result as an empty [][]int.
    2. Loop over masks from 0 up to but not including 1 << (n - 1), which is 2^(n-1). Every combination of n - 1 bits appears exactly once.
    3. Start fresh lists. Inside the loop, declare empty left and right slices, so nothing carries over from the previous mask.
    4. Visit the values. An inner loop runs i from 0 to n - 2, and val := i + 2 is the value that bit i controls.
    5. Test the bit and place the value. (mask>>i)&1 == 1 moves bit i to the lowest position and keeps only it. Append val to left if the bit is set, otherwise to right.
    6. Order the two sides: left descending, right ascending.
    7. Assemble the permutation: a copy of left, then 1, then the values of right. Append it to result.
    8. Return result after the outer loop ends.

    The solution in Go

    The code uses the sort package, so the file needs this import:

    import "sort"
    
    type Solution struct{}
    
    func (this Solution) generateValleyPermutations(n int) [][]int {
        result := [][]int{}
        for mask := 0; mask < (1 << (n - 1)); mask++ {
            left := []int{}
            right := []int{}
            for i := 0; i < n-1; i++ {
                val := i + 2
                if (mask>>i)&1 == 1 {
                    left = append(left, val)
                } else {
                    right = append(right, val)
                }
            }
            sort.Sort(sort.Reverse(sort.IntSlice(left)))
            sort.Ints(right)
            perm := append([]int{}, left...)
            perm = append(perm, 1)
            for _, v := range right {
                perm = append(perm, v)
            }
            result = append(result, perm)
        }
        return result
    }
    

    The Go details worth knowing:

    • Shifts and precedence. In Go, & and >> bind tighter than ==, so (mask>>i)&1 == 1 means ((mask>>i)&1) == 1. The comparison with 1 is required: an if condition must be a bool, and Go never treats the integer 1 as true.
    • Descending sort. sort.Ints sorts ascending only. For descending order, sort.IntSlice(left) turns the slice into a sortable type, sort.Reverse flips its comparison, and sort.Sort runs the sort in place.
    • Copying with append. append([]int{}, left...) copies the elements of left into a new slice. Writing perm := left would share the same array, and appending 1 could overwrite memory that belongs to left. Each stored permutation needs its own storage, because result keeps all of them.
    • Reassigning append. append may return a slice backed by a new array, so its result is always assigned back: left = append(left, val).
    • The receiver. The method has a value receiver on an empty struct, so it needs no state. Call it as Solution{}.generateValleyPermutations(4).
    • A negative shift count panics. 1 << (n - 1) with n = 0 shifts by -1, which stops the program with a run-time error. For n = 1 the shift is 0, the loop runs once with mask = 0, and the result is [[1]].

    Complexity

    There are 2^(n-1) masks. For each one, the inner loop does n - 1 steps and the two sorts cost up to O(n log n), and the permutation takes O(n) to build. The total time is O(2^n * n log n). Because the values are visited in increasing order, right and the reverse of left are already sorted, so the sorts only restate the required order; dropping them would lower the cost to O(2^n * n), which is also the size of the output.

    Space: the output holds 2^(n-1) permutations of length n, which for n = 10 is 512 permutations. Beyond the output, each iteration uses O(n) temporary space.

    Edge cases and common mistakes

    • n = 1. The only valley is [1].
    • n = 0. The shift panics. The mask approach needs n >= 1, so a defensive version would return early for smaller values.
    • Putting 1 anywhere else. Any other position makes the permutation fail the valley shape, because the minimum must be the bottom of the valley.
    • Forgetting the sorts or reversing them. Values on the left must decrease toward 1 and values on the right must increase away from it. A left side in ascending order produces a hill next to the 1.
    • Sharing storage. Reusing one slice across masks, or building perm without a copy, makes stored permutations change after they were appended.
    • Off-by-one in the bit loop. There are n - 1 bits, one for each of the values 2 to n. The loop bound is i < n-1, and the value is i + 2.

    Testing

    • generateValleyPermutations(1) gives [[1]].
    • generateValleyPermutations(4) gives the eight permutations from the example, in the same order.
    • len(generateValleyPermutations(10)) is 512.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free