Problem overview
The function countBinaryStrings receives two non-negative integers, n and k, and returns how many binary strings of length n contain no run of more than k consecutive ones. The statement guarantees k <= n <= 30.
For n = 4 and k = 2, there are 16 binary strings of length 4. Three of them break the limit: 0111, 1110 and 1111 all contain three ones in a row. The other 13 are valid, so the answer is 13.
Approach
Generating every string and checking it is O(2^n) work, and with n up to 30 that is over a billion strings. The answer itself is a count, so a better idea is to count strings without ever building them.
A valid string can be built one character at a time. Whether a new character is allowed depends on a single fact about the string so far: how many ones it ends with. That makes the state small. Let dp[i][j] be the number of valid strings of length i that end in exactly j consecutive ones. From a string counted there you can append:
- a
0, which is always allowed and resets the run, so the new string belongs todp[i+1][0]; - a
1, which extends the run toj + 1and is allowed only whilej < k, so the new string belongs todp[i+1][j+1].
Every valid string of length i + 1 comes from exactly one valid string of length i by removing its last character, so no string is counted twice or missed. The answer is the sum of the last row, dp[n][0] through dp[n][k].
For n = 4, k = 2 the table looks like this, with one row per length and one column per trailing-ones count:
i=0 [1 0 0]
i=1 [1 1 0]
i=2 [2 1 1]
i=3 [4 2 1]
i=4 [7 4 2] 7 + 4 + 2 = 13
You can check a few cells by hand. In row 2, dp[2][0] is 2: the strings 00 and 10 end in a zero. dp[2][1] is 1 (01), and dp[2][2] is 1 (11). Row 3 then gets dp[3][0] = 2 + 1 + 1 = 4, because a zero can be appended to every string of length 2. Column 2 of row 3 is dp[2][1], which is 1: only a string ending in exactly one 1 can grow into one ending in two. A string ending in two ones can't take another 1, since that would be a run of three.
Step by step
- Create the table
dpwithn + 1rows (lengths 0 ton) andk + 1columns (trailing ones 0 tok), filled with zeros. The lesson provides amakeMatrix(rows, columns)helper for this. - Seed the table:
dp[0][0] = 1. The empty string is the one valid string of length 0, and it ends in zero ones. - Loop over lengths
ifrom 0 ton - 1. The body writes into rowi + 1, and the last row isn. - Loop over trailing-ones counts
jfrom 0 tokinclusive. - Append a 0: add
dp[i][j]todp[i+1][0]. Several values ofjfeed this one cell, so it uses+=. - Append a 1 when allowed: if
j < k, adddp[i][j]todp[i+1][j+1]. - Declare
result := 0after the loops. - Add up the last row: loop
jfrom 0 tokand add everydp[n][j]toresult. - Return
result.
The solution in Go
func countBinaryStrings(n, k int) int {
dp := makeMatrix(n+1, k+1)
dp[0][0] = 1
for i := 0; i < n; i++ {
for j := 0; j <= k; j++ {
dp[i+1][0] += dp[i][j]
if j < k {
dp[i+1][j+1] += dp[i][j]
}
}
}
result := 0
for j := 0; j <= k; j++ {
result += dp[n][j]
}
return result
}
The Go details worth a closer look:
- Building a 2D slice. Go has no one-line constructor for a table of a given size. A
[][]intis a slice of slices, and each row has to be allocated separately. A helper like this one does it:
func makeMatrix(rows, cols int) [][]int {
m := make([][]int, rows)
for i := range m {
m[i] = make([]int, cols)
}
return m
}
- Zero values.
make([]int, cols)fills the row with zeros, which is why+=works on cells that were never assigned. - Shared parameter type.
n, k intdeclares both parameters asint. - Size.
intis 64 bits here, and the largest possible answer is2^30, which fits comfortably. - Index arithmetic.
dp[i+1][j+1]is why the guard isj < k: atj == kthe indexk + 1would be out of range, and Go stops the program with a panic instead of ignoring it.
Complexity
Time: O(n * k). The two nested loops run n times k + 1 times with constant work in each, and the final sum takes O(k). Space: O(n * k) for the table. Since row i + 1 depends only on row i, two rows would be enough, but the lesson keeps the whole table to make the idea easy to follow.
Edge cases and common mistakes
k = 0. No ones are allowed, so exactly one string qualifies: all zeros.countBinaryStrings(3, 0)returns1. The table has a single column and thej < kbranch never runs.k = n. Every string is valid, so the answer is2^n.countBinaryStrings(3, 3)returns8.n = 0. The only string is the empty one, and the answer is1.- Looping
j < kinstead ofj <= k. A string may end in exactlykones, so columnkmust be processed and included in the final sum. Missing it undercounts. - Using
=instead of+=fordp[i+1][0]. Eachjoverwrites the previous contribution instead of adding to it. - Looping
iup ton. The body writes to rowi + 1, which doesn't exist fori == n, so the program panics.
Testing
countBinaryStrings(4, 2)gives13.countBinaryStrings(3, 1)gives5: the strings with no two adjacent ones are 000, 001, 010, 100 and 101.countBinaryStrings(3, 0)gives1.countBinaryStrings(3, 3)gives8.countBinaryStrings(0, 0)gives1.