1.4.34

1.4.34 #

解答 #

第一种方案,类似于二分查找,先猜测左边界(lo),再猜测右边界(hi),如果边界值猜中的话直接返回,否则:

如果右边界比较热,那么左边界向右边界靠,lo=mid;否则,右边界向左边界靠,hi=mid。其中,mid = lo + (hi – lo)/2。

每次二分查找都要猜测两次,~2lgN。

第二种方案,假设上次猜测值为 $lastGuess$,本次即将要猜测的值为 $nowGuess$,通过方程:

$(lastGuess + nowGuess)/2 = (lo + hi)/2$

可以求得 $nowGuess$,具体可以查看示意图:

数字是猜测顺序,黑色范围是猜测值的范围($lastGuess$ 和 $nowGuess$),绿色的是实际查找的范围(lo 和 hi)。

代码 #

首先是 Game 类

internal class Game
{
    public int N { get; }                       // 目标值的最大范围。
    public int SecretNumber { get; }            // 目标值。
    public int LastGuess { get; private set; }  // 上次猜测的值

    /// <summary>
    /// 构造函数,新开一局游戏。
    /// </summary>
    /// <param name="n">目标值的最大范围。</param>
    public Game(int n)
    {
        var random = new Random();
        N = n;
        SecretNumber = random.Next(n - 1) + 1;
        LastGuess = -1;
    }

    /// <summary>
    /// 猜测,根据与上次相比更为接近还是远离目标值返回结果。
    /// </summary>
    /// <param name="guess">本次的猜测值</param>
    /// <returns>接近或不变返回 Hot,远离则返回 Cold,猜中返回 Equal。</returns>
    public GuessResult Guess(int guess)
    {
        if (guess == SecretNumber)
        {
            return GuessResult.Equal;
        }
        if (LastGuess == -1)
        {
            LastGuess = guess;
            return GuessResult.FirstGuess;
        }

        var lastDiff = Math.Abs(LastGuess - SecretNumber);
        LastGuess = guess;
        var nowDiff = Math.Abs(guess - SecretNumber);
        if (nowDiff > lastDiff)
        {
            return GuessResult.Cold;
        }

        return GuessResult.Hot;
    }

    /// <summary>
    /// 重置游戏,清空上次猜测的记录。目标值和最大值都不变。
    /// </summary>
    public void Restart()
    {
        LastGuess = -1;
    }
}

之后是实际测试的方法:

var game = new Game(1000);
var a = PlayGameA(game);
game.Restart();
var b = PlayGameB(game);
Console.WriteLine($"SecretNumber:{game.SecretNumber}");
Console.WriteLine("TestResultA:");
Console.WriteLine($"SecretNumber:{a.SecretNumber}, TryTimes:{a.TryTimes}");
Console.WriteLine();
Console.WriteLine("TestResultB:");
Console.WriteLine($"SecretNumber:{b.SecretNumber}, TryTimes:{b.TryTimes}");

// 方案一,用二分查找实现,需要猜测 2lgN 次。
static TestResult PlayGameA(Game game)
{
    TestResult result;
    result.TryTimes = 0;
    result.SecretNumber = 0;

    var hi = game.N;
    var lo = 1;

    // 利用二分查找猜测,2lgN
    while (lo <= hi)
    {
        var mid = lo + (hi - lo) / 2;

        var guessResult = game.Guess(lo);
        result.TryTimes++;
        if (guessResult == GuessResult.Equal)
        {
            result.SecretNumber = lo;
            return result;
        }

        guessResult = game.Guess(hi);
        result.TryTimes++;
        if (guessResult == GuessResult.Equal)
        {
            result.SecretNumber = hi;
            return result;
        }

        if (guessResult == GuessResult.Hot)
        {
            lo = mid;
        }
        else
        {
            hi = mid;
        }
    }

    return result;
}

// 方案二,根据 (lastGuess + nowGuess)/2 = (lo + hi) / 2 确定每次猜测的值。
static TestResult PlayGameB(Game game)
{
    TestResult result;
    result.TryTimes = 0;
    result.SecretNumber = 0;

    var hi = game.N;
    var lo = 1;
    var isRightSide = true;

    // 第一次猜测
    var guessResult = game.Guess(1);
    result.TryTimes++;
    if (guessResult == GuessResult.Equal)
    {
        result.SecretNumber = 1;
        return result;
    }

    while (lo < hi)
    {
        var mid = lo + (hi - lo) / 2;
        var nowGuess = (lo + hi) - game.LastGuess;

        guessResult = game.Guess(nowGuess);
        result.TryTimes++;
        if (guessResult == GuessResult.Equal)
        {
            result.SecretNumber = nowGuess;
            break;
        }

        if (guessResult == GuessResult.Hot)
        {
            if (isRightSide)
            {
                lo = mid;
            }
            else
            {
                hi = mid;
            }
        }
        else
        {
            if (isRightSide)
            {
                hi = mid;
            }
            else
            {
                lo = mid;
            }
        }

        isRightSide = !isRightSide;
        if (hi - lo <= 1)
        {
            break;
        }
    }

    if (game.Guess(lo) == GuessResult.Equal)
    {
        result.TryTimes++;
        result.SecretNumber = lo;
    }
    else if (game.Guess(hi) == GuessResult.Equal)
    {
        result.TryTimes++;
        result.SecretNumber = hi;
    }

    return result;
}

/// <summary>
/// 某种方案的测试结果,包含猜测结果和尝试次数。
/// </summary>
internal struct TestResult
{
    public int SecretNumber; // 猜测到的数字。
    public int TryTimes; // 尝试次数。
}