Problem overview
The function count_binary_strings takes a length n and returns how many binary strings of exactly that length exist, where a binary string is made only of the characters 0 and 1. The statement limits N to 30.
For n = 3 the strings are 000, 001, 010, 011, 100, 101, 110 and 111, so the answer is 8.
Approach: build each length from the previous one
Listing the strings would work for tiny inputs, but the count grows fast, and counting is easier than listing. Let dp[i] be the number of binary strings of length i. Every string of length i is a string of length i - 1 followed by one more character, and that character is either 0 or 1. Each shorter string can therefore be extended in exactly two ways, and no two different extensions give the same result. So:
dp[i] = 2 * dp[i - 1]
With the base case dp[1] = 2 (the strings "0" and "1"), the table runs 2, 4, 8, 16 and so on. This is dynamic programming in its simplest form: a table of answers for small sizes, and a rule that gets each entry from earlier ones. The closed form is 2^n, but the table shows the pattern that harder problems in this chapter build on.
Step by step
- Create
dpwithvec![0; n + 1], declaredlet mutbecause the entries will change. Indexistores the count for lengthi, so the vector needsn + 1slots for indexnto exist. The element type is inferred asi32from the function's return type. - Set the base case
dp[1] = 2. - Loop over the lengths from 2 to
nwith2..=n. The=makes the range includen. Filling in increasing order guarantees thatdp[i - 1]is ready when it is needed. - Double the previous entry:
dp[i] = dp[i - 1] * 2. - Return
dp[n]as the final expression, without a semicolon.
The solution in Rust
impl Solution {
fn count_binary_strings(n: usize) -> i32 {
let mut dp = vec![0; n + 1];
dp[1] = 2;
for i in 2..=n {
dp[i] = dp[i - 1] * 2;
}
dp[n]
}
}
The Rust details worth a closer look:
nis ausize. Lengths and indices in Rust areusize, son + 1anddp[n]need no conversion. The result type is stilli32.- Inclusive and exclusive ranges.
2..=nvisits 2, 3, ..., n. Writing2..nwould stop atn - 1and return the entry for the wrong length. Whennis 1,2..=1is an empty range, so the loop doesn't run. - Indexing is checked. Reading or writing past the end of a vector panics instead of touching other memory.
- Overflow. The count for length 30 is 1,073,741,824, which fits in an
i32. Length 31 would need 2,147,483,648, one more thani32::MAX. In an optimized build the arithmetic wraps silently, and the function would return -2147483648. In a debug build it would panic instead. The limit N ≤ 30 keeps the code on the safe side.
Complexity
Time: O(n). The loop runs at most n - 1 times, with one multiplication each.
Space: O(n). The table has n + 1 entries. Since each entry only needs the previous one, a single running variable would reduce this to O(1), but the table is the habit this lesson is building.
Edge cases and common mistakes
- N = 1. Returns 2 straight from the base case, and the loop is skipped.
- N = 0. The empty string is the one string of length 0, so the true count is 1. This code doesn't handle it:
dphas length 1, anddp[1] = 2panics with "index out of bounds: the len is 1 but the index is 1". A version that setsdp[0] = 1and starts the loop at 1 would cover it. - Exclusive range.
2..nleavesdp[n]at 0 for everynabove 1. - Allocating only
nentries.vec![0; n]has no indexn, so the finaldp[n]panics. - Adding instead of multiplying.
dp[i - 1] + 2would give 2, 4, 6, 8 and miss the doubling.
Testing
n = 1gives 2.n = 3gives 8.n = 10gives 1024.n = 30gives 1073741824.
