沙漏问题

有两个沙漏:一个需要\(4\)分钟跑完,另一个需要\(7\)分钟。当第一次启动时钟时必须开始测量时,如何精确测量\(9\)分钟?另一个时钟、数秒和猜测是不允许的。您所要做的就是将沙漏翻过来,观察上层室何时变空。转身不应该花任何时间。我们正在寻找的是一个过程,在这个过程中,所期望的时间段的结束只能通过沙漏来识别。


我们将具有\(4\)分钟的时钟称为\(A\) ,将具有\(7\)分钟的时钟称为\(B\) 。最初所有的沙子都在底部。我们通过翻转两个时钟来同时启动它们;从此时起,搜索的\(9\)分钟开始计数。之后,当顶部的时钟变空时,您可以随时进行交易。您不得翻转一个、一个或两个时钟,即使是仍在运行的时钟。你等到下次它用完的时候。不允许停车和标记;空钟可以保持站立状态。解决方案看起来像这样:

  1. 在\(0\)分钟\(A\)和\(B\)启动。
  2. 在分钟\(4\) \(A\)为空并被翻转。 \(B\)继续运行不变。
  3. 在分钟\(7\) \(B\)为空并被翻转。 \(A\)还剩\(1\)分钟。
  4. 在分钟\(8\) \(A\)为空。在\(B\)中,沙子流下整整\(1\)分钟;因此\(B\)立即又被翻过来。
  5. 在分钟\(9\) \(B\)为空。您要寻找的时间已到。

在\(8\)分钟\(B\) 。自从它们在\(7\)分钟重新启动以来,流下的沙子数量与在\(1\)分钟内通过瓶颈的沙子数量完全相同。翻转后,只有这一小部分在上面。现在需要\(1\)分钟才能再次滴落。因此,时钟在\(9\)分钟变空,而不是在另一次完整运行之后。所以我们用已经通过的沙子来测量最后一刻的情况。动画展示了这个动作:

翻转运行的沙漏对于一般解决方案也很有用。剩余时间是指沙子仍然需要流经上室的时间。如果满钟的运行时间为\(c\) ,剩余运行时间为\(r\) ,则底部的沙子数量对应于持续时间\(cr\) 。当你把它翻过来时,这个确切的数字就会出现在顶部。如果上腔室事先已满,则随后将是空的,反之亦然。对于部分运行的时钟,新的剩余运行时间为\(r'\):

$$r'=c-r.$$

以前,测量从第一圈开始。这意味着“没有提前时间”:自开始以来的每一分钟都很重要。另一方面,“提前”意味着您可以先准备好时钟,然后再开始所需的时间段。例如,我们启动两个时钟并在\(4\)分钟转动\(A\) 。如果\(B\)在\(7\)分钟变空,我们的测量就开始。如果\(A\)在\(8\)分钟变空,则结束。我们测量了\(1\)分钟;之前的\(7\)分钟不算在内。

现在我们考虑具有正整数运行时间\(a\)和\(b\)的任意沙漏。我们将所需的持续时间称为\(T\) 。如果准备允许的话,可以用这个来测量什么时间?在这里,开始和结束也必须通过空白来识别。因此,您不应该根据自己的感觉在某个时刻开始进行测量。手柄与实际拼图中的手柄保持相同。首先我们显示哪些时间被排除;然后我们将向您展示如何实际测量其余所有内容。

首先让我们使用\(6\)和\(10\)分钟的时钟。两个运行时间都可以被\(2\)整除。因此,沙子可以在心里分成几部分,每一部分都需要\(2\)分钟才能通过。当您将其翻转或等到下一次它为空时,只会再次创建整个部分。因此,所有可观察到的空闲时间相隔偶数分钟。即使您事先准备好时钟,也无法使用它来测量\(3\)分钟的持续时间。

一般来说,令\(g=\gcd(a,b)\)为运行时间的最大公约数。最初剩余的两项都是\(g\)的倍数。当旋转 时\(r\)变为\(ar\)或\(br\) ;两者都可以被\(g\)整除。等待时,经过最小的正剩余时间,也是\(g\)的倍数。这意味着所有空时间及其距离仍然是\(g\)的倍数。因此,可测量的持续时间必须满足\(g\mid T\) : \(T\)必须能被\(g\)整除。这是否足够还有待观察。

为此,我们让两个时钟一次又一次完全运行,并在每个时钟空了后立即将其翻转。第一个在\(a,2a,3a,\ldots\)个时间单位后变空,第二个在\(b,2b,3b,\ldots\)后变空。我们正在寻找第二个时钟的时间\(mb\)和第一个时钟的较晚时间\(na\) ,两者之间恰好是\(T\) 。如果\(T\)是\(g\)的正倍数,Bézout 引理保证匹配整数\(n,m\) 。您正在寻找的关系是::

$$na-mb=T.$$

Bézout 提供的数字\(n,m\)最初可以为负数。不能进行负运行。然而,我们可以在不改变时间间隔的情况下增加这两个数字:我们用\(n+kb/g\)和\(m+ka/g\)替换它们,其中\(k\)是一个足够大的正整数。因此,两个时间都偏移相同的量\(kab/g\) ;他们的区别仍然是\(T\) 。因此,两个数字都变为正值,所需时间实际上是在时钟开始之后。

现在您可以计算每个时钟的完整周期。当\(m\)时钟变空时,测量开始;当第一个时钟第\(n\)次变空时,它结束。其间正好有\(T\)个时间单位。准备工作可能需要很长时间。该证明并没有承诺一个特别短的过程。对于\(T=0\)起点和终点重合。这证明了所有非负整数目标:通过准备,当且仅当最大公约数除以持续时间时,才可以测量持续时间:

$$T\text{ ist messbar}\quad\Longleftrightarrow\quad \gcd(a,b)\mid T.$$

此规则不一定适用于第一次开始时就开始测量。例如, \(4\)和\(7\)分钟\(g=1\) ,但在\(1\)分钟后没有时钟为空。所以你看不到这一分钟的结束。上述从\(7\)到\(8\)分钟的测量仅有效,因为准备工作不算在内。因此,这个证明回答了准备之后什么是可能的问题。如果您正在寻找有或没有准备的许可流程,以下系统搜索将会有所帮助。

PHP 脚本会记住剩余运行时间和自测量开始以来已经过去的时间。对于每一个这样的条件,都尝试一下 \(4\) 可能性:没有时钟,只有第一个,只有第二个,或者两者都翻转。然后它会等待,直到下次清空。价值 \(-1\) 表示准备;条目 start 设置测量时间 \(0\). 已知情况不会重新调查。找到的序列是通过其前身获得的。 wait 仅指到下一次排空的距离,而不是额外的时间测量:

<?php
declare(strict_types=1);

function hourglass(int $a, int $b, int $target, bool $prepare = false): ?array
{
    if ($a <= 0 || $b <= 0 || $target < 0) {
        throw new InvalidArgumentException('Positive capacities, nonnegative target required.');
    }
    $root = [0, 0, $prepare ? -1 : 0];
    $key = static fn(array $s): string => implode(',', $s);
    $queue = new SplQueue();
    $queue->enqueue($root);
    $previous = [$key($root) => null];
    $enqueue = static function (array $next, array $from, array $action)
        use ($queue, &$previous, $key): void {
        $k = $key($next);
        if (!array_key_exists($k, $previous)) {
            $previous[$k] = [$key($from), $action];
            $queue->enqueue($next);
        }
    };
    while (!$queue->isEmpty()) {
        $state = $queue->dequeue();
        [$x, $y, $elapsed] = $state;
        if ($elapsed === $target) {
            $path = [];
            for ($k = $key($state); $previous[$k] !== null;) {
                [$k, $action] = $previous[$k];
                $path[] = $action;
            }
            return array_reverse($path);
        }
        if ($elapsed === -1) {
            $enqueue([$x, $y, 0], $state, ['start' => true]);
        }
        for ($mask = 0; $mask < 4; ++$mask) {
            $u = ($mask & 1) ? $a - $x : $x;
            $v = ($mask & 2) ? $b - $y : $y;
            if ($u === 0 && $v === 0) {
                continue;
            }
            $wait = $u === 0 ? $v : ($v === 0 ? $u : min($u, $v));
            $nextTime = $elapsed < 0 ? -1 : $elapsed + $wait;
            if ($nextTime > $target) {
                continue;
            }
            $enqueue(
                [max(0, $u - $wait), max(0, $v - $wait), $nextTime],
                $state,
                ['flip' => $mask, 'wait' => $wait]
            );
        }
    }
    return null;
}

foreach ([[4, 7, 9, false], [4, 7, 1, false], [4, 7, 1, true],
          [6, 10, 3, true], [5, 5, 10, false], [4, 7, 0, false]] as $case) {
    echo json_encode($case), ': ', json_encode(hourglass(...$case)), PHP_EOL;
}

搜索找到或返回允许的进程 null, 如果不存在。它涵盖了所有允许的延续并需要最大限度的准备 \((a+1)(b+1)(T+2)\) 国家。因此,大输入需要大量内存。搜索转换的次数被最小化,而不是准备时间被最小化。合理的跑步时间和目标时间使用公分母转换为整数。例如,对于没有公共时间单位的运行时间 \(1\) 和 \(\sqrt{2}\), 这种有限整数搜索是不合适的。

背部