I decided to create a small program that would just find a number in a given range using what I think is binary search. The code works well, as far as I can tell. The issue appeared when I wanted to make a mathematical function that would predict the average amount of guesses needed to guess the given number, and rather intuitively, the function was:
log2(x) + 1
However, when testing the code experimentally, the result on large numbers like 1e12 resembled more the formula:
log2(x) - 1
Also, it could be that the formula is not correct as I have my doubts about it. Anyway here is the code, in case you find any issues with the calculations:
use rand::Rng;
use std::io;
use std::cmp::Ordering;
use std::time::Instant;
fn read_input() -> i64 {
println!("Insert the range of the number [1 - <input>]: ");
let mut max_range_str = String::new();
io::stdin()
.read_line(&mut max_range_str)
.expect("Couldn't read the line");
let max_range = max_range_str
.trim()
.parse()
.expect("Couldn't convert to int");
println!("");
max_range
}
fn create_secret_number(max_range: i64) -> i64 {
rand::thread_rng().gen_range(1..=max_range)
}
fn main() {
let max_range = read_input();
let mut loop_count = 0;
let mut sum_of_guesses = 0;
loop {
let start_time = Instant::now();
let secret_number = create_secret_number(max_range);
let mut iterations = 0;
let mut ceiling = max_range;
let mut floor = 0;
let mut guess = max_range / 2;
loop {
iterations += 1;
match guess.cmp(&secret_number) {
Ordering::Greater => ceiling = guess,
Ordering::Less => floor = guess,
Ordering::Equal => {
let elapsed_time = start_time.elapsed();
println!("The correct number was {}", secret_number);
println!("You win after {} iterations in {:?} seconds \n", iterations, elapsed_time);
sum_of_guesses += iterations;
break;
}
}
guess = (ceiling + floor) / 2;
}
loop_count += 1;
let average_guesses = sum_of_guesses / loop_count;
println!("Average guess is now: {}", average_guesses);
}
}
I'm learning Rust myself [a disclaimer you shouldn't be inspired by my coding conventions].
Let's start with a simple algorithm. Start with a range from
1to some power of2. I'm assuming all ranges are a power of 2. Under this assumption, your code positions the guess on the maximum of 1st half. So if the guess is lower than the secret number, you guessed the right half. Then you can divide this half into its own two halves and repeat the process:I modified your code fixing some errors, and removing console printing, which is so slow, you're probably better off iterating over a million tries at once instead of continuously iterating and waiting until you reach the million eventually:
Keep in mind I could just replace the
max_rangeinfor iterations in 0..max_rangewith a log₂ of the maximum, but then I would be assuming what I'm proving… Perhaps obviously, the result is an average of log₂ guesses (e.g. 7 for a range of 128), because that's the number of subdivisions (128, 64, 32, 16, 8, 4, 2; the last step of the iteration is not counted because there's nothing to guess).Your code, however, has a small chance, to break early:
So as you can see, the bigger the range, the more it approaches 100% chance to break early. Except, as I already said, it's unfair to say you can break early from the innermost pair (e.g. 1..2), because that would be the last guess anyway. Therefore, given the assumption in bold, you're approaching 50% chance to break early.
If for some erroneous reason you would assume that breaking early means decreasing the
sum_of_guessesby just1, then the bigger the range, the closer the actual average would be to the log₂max - 0.5. However, perhaps again obviously, the earlier you break, the more guesses you avoid:and so on, approaching one guess saved.