Start learning free

LESSON 269 OF 348 · BACKTRACKING

Binary Strings With K Ones in Kotlin: Backtracking

Practice O(2^n) Read the full explanation

Given two non-negative integers N and K, return the binary strings of length N with exactly K ones

Example:

Input: N = 4, K = 2
Output: [
    "1100",
    "1010",
    "1001",
    "0110",
    "0101",
    "0011"
]
Notes: K <= N <= 20
STEP 1 OF 11
    Ctrl + Enter to check

    KOTLIN · BACKTRACKING

    Generate Binary Strings With K Ones in Kotlin, explained

    Problem overview

    Given two non-negative integers n and k, generateBinaryStrings returns every binary string of length n that contains exactly k ones. The lesson states k <= n <= 20.

    For n = 4 and k = 2 there are six strings: 0011, 0101, 0110, 1001, 1010 and 1100. The result in this solution comes out in this order, smallest first. The order of the result is not what matters, only that each string appears once.

    Approach

    Build the string from left to right. At each position we choose '0' or '1', which gives a binary tree of choices with depth n. A leaf at depth n is a complete string, and we keep it only if it has exactly k ones.

    To count the ones without scanning each string, pass a budget k down the recursion: the number of ones we may still place. Appending '0' leaves the budget as it is. Appending '1' spends one, so the budget becomes k - 1. Then:

    • if the budget drops to -1, we have placed too many ones, so the whole branch is abandoned;
    • if the string reaches length n with a budget of exactly 0, it holds exactly k ones and is a result;
    • a full-length string with budget above 0 has too few ones and is discarded.

    Step by step

    1. A shared list. solutions is a lateinit var of type MutableList<String>. It has no value until generateBinaryStrings runs, which is exactly what lateinit is for.
    2. Reset it. At the top of generateBinaryStrings, assign mutableListOf(). The element type is inferred from the property's declared type, and a fresh list keeps old results out.
    3. Start the recursion with generate("", n, k): an empty prefix, the target length and the full budget. Then return solutions.
    4. Declare generate with current: String, n: Int and k: Int.
    5. Prune. If k == -1, return: this branch already has one '1' too many.
    6. Handle a full string. If current.length == n, add current to solutions only when k == 0, and return in either case so a string never grows beyond n characters. In Kotlin length is a property, written without parentheses.
    7. Branch. Recurse with current + '0' and the same k, then with current + '1' and k - 1. Trying '0' first produces the strings in increasing order.

    The solution in Kotlin

    class Solution {
        fun generate(current: String, n: Int, k: Int) {
            if (k == -1) {
                return
            }
            if (current.length == n) {
                if (k == 0) {
                    solutions.add(current)
                }
                return
            }
            generate(current + '0', n, k)
            generate(current + '1', n, k - 1)
        }
    
        lateinit var solutions: MutableList<String>
        fun generateBinaryStrings(n: Int, k: Int): List<String> {
            solutions = mutableListOf()
            generate("", n, k)
            return solutions
        }
    }
    

    Kotlin details:

    • current + '0' creates a new string and leaves current unchanged. That is why the second recursive call can use current again, and why there is no undo step.
    • Chars and strings. '0' is a Char and "0" is a String. Adding a Char to a String with + appends the character, so "ab" + '0' is "ab0". It is not arithmetic: nothing is added numerically.
    • lateinit var reads like a normal property but has no starting value. Reading it too early throws UninitializedPropertyAccessException.
    • Return type. The function returns List<String>. The MutableList stored in solutions is also a List, so it can be returned as it is.
    • Early return. A bare return leaves a function that returns Unit, which is the default when you write no return type.

    Following n = 3, k = 1

    The search starts from the empty string with a budget of 1. Appending '0' three times gives 000 with budget 1, which is discarded because the budget is not 0. Backing up one level, 00 followed by '1' gives 001 with budget 0, which is kept. The branch 01 continues with '0' to reach 010, kept, while 011 brings the budget to -1 and is cut at once. The branch 1 has budget 0, so only the zeros after it survive and 100 is kept. The result is ["001", "010", "100"].

    Complexity

    The recursion tree has up to 2^n leaves, because for each of the n positions we try both characters. The budget check cuts only branches with too many ones, so the search still visits strings with too few ones and discards them at the end. Each leaf costs O(n) to build. For n = 20 that is at most about a million leaves. The result list holds C(n, k) strings of length n, for example 184756 strings for n = 20 and k = 10. The recursion is n levels deep.

    A tighter version would also stop a branch when the budget is larger than the number of positions left, because such a branch can never reach exactly k ones. The lesson's version keeps the code simple.

    Edge cases and common mistakes

    • k = 0. There is exactly one string, all zeros. generateBinaryStrings(3, 0) returns ["000"].
    • k = n. Exactly one string, all ones: generateBinaryStrings(3, 3) returns ["111"].
    • k larger than n. The lesson rules it out, but the code handles it: no string can match, and generateBinaryStrings(2, 5) returns an empty list.
    • n = 0. The empty string has length 0 and, with k = 0, matches, so the result is a list holding one empty string.
    • Accepting a full string with a leftover budget. Without the k == 0 check, you would also return strings with too few ones.
    • Forgetting to return after the length check. The string would keep growing past length n, and the recursion would never end.
    • Forgetting the reset. A second call would return the earlier strings along with the new ones.

    Testing

    • generateBinaryStrings(4, 2) returns ["0011", "0101", "0110", "1001", "1010", "1100"].
    • generateBinaryStrings(3, 0) returns ["000"].
    • generateBinaryStrings(3, 3) returns ["111"].
    • generateBinaryStrings(20, 10).size is 184756.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free