50. 绳子和剪刀

这是之前遇到过的一个面试题,说实话,当时想到了开头,但没能进行下去,毕竟循环的条件和结束循环的条件并不容易找。而且要求在纸上手写代码,有点紧张,错误连篇。当他说完的那一刻,我已经在心里默默算出了答案,但似乎跟他的要求不相符,就一直没能说出来。

题目是这样的:有一根绳子,奇数长度时从一端减去1米长的一段,偶数长度时从中间剪断,你写一个程序,把这根绳子全部剪成1米长的小段,需要几刀?

刀数是显而易见的,就是绳子的长度减1,但是无法表达出如何实现这一过程,我写了开头,就没能在写下去。今天刚好在家没事,突然想起这个问题,觉得肯定能用代码描述这个过程。想了整整一个下午,都没有解决这个问题,后来我还画了这个图

想是不是将一根绳子拆分成类似一棵树会简单一些,即统计树的最底下一层的1个数就可以知道最终剪了几刀。但是用PHP实现这样一棵树,显然是把这个问题想复杂了,不应该有这么复杂的,但是递归一时半会又不知道怎么实现。

分解绳子长度

去超市回来后,突然就把结果跑出来了,才回头去理这一过程。程序就是这么微妙,想到了豁然开朗,简单,想不到,就手足无措,写代码确实很烧脑。

function cut($length)
{
    $count = 0;
    // 按递减的绳子长度遍历
    while ($length >= 1) {
        echo sprintf("当前绳子长度 %d 米\n", $length);
        // 偶数长度时从中间剪断
        if ($length % 2 == 0) {
            // 每次处理绳子长度的一半
            echo sprintf("每次处理绳子长度的一半, 绳子长度 %d 米, 一半长度 %d 米\n", $length, $length / 2);
            // 该递归处理绳子长度的一半, 直到计算出该半截绳子所使用的刀数为止
            $count += cut($length / 2);
            // 接下来该一半绳子继续循环上一次的操作路线, 完成后半截绳子所使用的刀数
            $length /= 2;
        // 奇数长度时从一端减去1米长, 记录1刀
        } else {
            // 绳子长度为1米时记录刀数1刀
            echo sprintf("绳子长度: %d 米, 从一端减去1米,剩余: %d 米\n", $length, $length - 1);
            $length--;
            $count++;
        }
    }

    return $count;
}

// 默认1米长的绳子
$num = $argv[1] ?? 1;
echo sprintf("当前绳子长度为: %d 米, 裁剪开始...\n\n", $num);
echo sprintf("\n裁剪结束, 该绳子一共剪了 %d 刀\n", cut($num) - 1);

当场我在纸上写时,其实已经写到递归这一步了的,但是已是没明白递归的关系,觉得两半截绳子,应该执行两次递归的,可是递归一旦执行,就有可能被返回,另一段绳子就得不到执行。还是今天才明白该处只需要一次递归就可,下一次的循环条件其实就进行了后半截绳子的递归。

举几个例子就明白了,绳子的长度为3米时的递归过程:

zhgxun-pro:php zhgxun$ php cut.php 3
当前绳子长度为: 3 米, 裁剪开始...

当前绳子长度 3 米
绳子长度: 3 米, 从一端减去1米,剩余: 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米

裁剪结束, 该绳子一共剪了 2 刀
zhgxun-pro:php zhgxun$

绳子的长度为5米时的递归过程:

zhgxun-pro:php zhgxun$ php cut.php 5
当前绳子长度为: 5 米, 裁剪开始...

当前绳子长度 5 米
绳子长度: 5 米, 从一端减去1米,剩余: 4 米
当前绳子长度 4 米
每次处理绳子长度的一半, 绳子长度 4 米, 一半长度 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米

裁剪结束, 该绳子一共剪了 4 刀
zhgxun-pro:php zhgxun$

绳子的长度为15米时的递归过程:

zhgxun-pro:php zhgxun$ php cut.php 15
当前绳子长度为: 15 米, 裁剪开始...

当前绳子长度 15 米
绳子长度: 15 米, 从一端减去1米,剩余: 14 米
当前绳子长度 14 米
每次处理绳子长度的一半, 绳子长度 14 米, 一半长度 7 米
当前绳子长度 7 米
绳子长度: 7 米, 从一端减去1米,剩余: 6 米
当前绳子长度 6 米
每次处理绳子长度的一半, 绳子长度 6 米, 一半长度 3 米
当前绳子长度 3 米
绳子长度: 3 米, 从一端减去1米,剩余: 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 3 米
绳子长度: 3 米, 从一端减去1米,剩余: 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 7 米
绳子长度: 7 米, 从一端减去1米,剩余: 6 米
当前绳子长度 6 米
每次处理绳子长度的一半, 绳子长度 6 米, 一半长度 3 米
当前绳子长度 3 米
绳子长度: 3 米, 从一端减去1米,剩余: 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 3 米
绳子长度: 3 米, 从一端减去1米,剩余: 2 米
当前绳子长度 2 米
每次处理绳子长度的一半, 绳子长度 2 米, 一半长度 1 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米
当前绳子长度 1 米
绳子长度: 1 米, 从一端减去1米,剩余: 0 米

裁剪结束, 该绳子一共剪了 14 刀
zhgxun-pro:php zhgxun$

问题其实简单,但也不是那么容易做出来,关键一开始要是能考虑到循环开始和结束的条件,后面的过程就很容易进行下去了。